2015年7月29日水曜日

2Dアニメ機能のライブラリを試作(本編1)

2Dアニメーションのライブラリを試作した話の続きです。UIViewのサブクラスという形を選択し、いろいろなViewと組み合わせて使えることを想定して作りました。

いよいよソースコードの紹介なのですが、クラス本体の紹介の前に、データ定義を紹介します。指定する項目が多すぎて、かなり長いSwiftコードになってしまいました。

 

最初に作ったデータ定義は、たった1つの構造体で、多くの項目が含まれていました。テストコードを作るのが書きづらく感じ、すぐに分割することにしました。

まず2つに分けようと思ったのですが、上手に分ける基準が見付かりません。分割の試行錯誤を重ねているうちに、3つに分けるのが適切ではないかと思うようになりました。表示する要素(文字列や画像など)に関する情報、アニメーションの共通情報(時間や繰り返し回数など)、2Dアニメーションで指定する情報(位置や回転や透明度など)の3つです。

3つに分けたのですが、構造体名の番号はゼロから始まります。3つめが2Dアニメーションの指定情報で、2番を割り当てられるからです。また、3Dアニメーションで指定する情報の構造体を追加したとき、3番が割り当てられるのも良い点だと考えました。

 

データ定義の構造体を紹介する前に、構造体の中に含まれる項目のうち、列挙型に適したものを選んで、最初に定義しました。次のように、3つあります。

// UI部品の種類
enum ZavType: Int {
    case ZtView = 0
    case ZtImg = 1
    case ZtText = 2
}
// アニメーションの種類
enum ZavAnimate: Int {
    case Za2D = 0 // 2Dアニメ
    case Za3D = 1 // 3Dアニメ
    case ZaEtc = 2 // 特殊アニメ(画像処理など)
}
// 動作速度変化(Timing Functions)の種類
enum ZavTF: Int {
    case ZpLinear = 0
    case ZpIn = 1
    case ZpOut = 2
    case ZpInOut = 3
}

最初は、アニメーションとして動かす表示要素の種類です。汎用的に使うUIVew、画像のUIImage、文字列のStringの3つだけです。UIImageもStringも、最終的にはUIViewの形で生成するのですが、もとの形のままでライブラリへ渡せたほうが使いやすいので、このように作ってみました。

2つめのアニメーションの種類は、今のところ、3種類を考えています。2Dと3Dとその他です。その他は、画像処理を加えながらのアニメーションを考えていますが、細かな内容はまだ未定です。3種類のうち、最初は2Dだけを作りました。

3つめの動作速度変化の種類は、iOSのAPIに用意されている設定項目「Timing Functions」です。時間の変化に比例して動くLinearのほかに、動き出しがゆっくりのIn、終わりがゆっくりのOutなどがあります。この項目が指定できると、アニメーションに速度の変化が付けられて、表現力が向上します。必要だと感じて入れました。

 

準備が終わったので、いよいよデータ定義の構造体です。3つあるので、順番に取り上げます。

最初の構造体は、表示する要素(文字列や画像など)の関する情報です。

struct ZavDef0 {
    var sType: ZavType
    var sAnim: ZavAnimate
    var sObj: AnyObject? = nil
    var sText: String = "dummy"
    var sTSize: CGFloat = 24
    var sTColor: UIColor = UIColor.blackColor()
    var sFuncI: ((UIView) -> Void)? = nil   // 表示部品に対する初期化時の加工処理:処理せずはnil
    init(sType:ZavType, sAnim:ZavAnimate, sObj:AnyObject?, sFuncI:((UIView) -> Void)?) {
        self.sType = sType
        self.sAnim = sAnim
        self.sObj = sObj
        self.sFuncI = sFuncI
    }
    init(sType:ZavType, sAnim:ZavAnimate, sObj:AnyObject?) {
        self.sType = sType
        self.sAnim = sAnim
        self.sObj = sObj
    }
    init(sAnim:ZavAnimate, sTSize:CGFloat, sText:String, sTColor:UIColor, sFuncI:((UIView) -> Void)?) {
        self.sType = .ZtText
        self.sAnim = sAnim
        self.sText = sText
        self.sTSize = sTSize
        self.sTColor = sTColor
        self.sFuncI = sFuncI
    }
    init(sAnim:ZavAnimate, sTSize:CGFloat, sText:String, sTColor:UIColor) {
        self.sType = .ZtText
        self.sAnim = sAnim
        self.sText = sText
        self.sTSize = sTSize
        self.sTColor = sTColor
    }
}

最初の2項目は、列挙型で定義した内容なので、説明不要でしょう。

続くsObjは、UIViewまたはUIImageを受け取るために使います。文字列では使いません。文字列のときにnilとなるので、Optional型にして、デフォルト値としてnilを入れてあります。

続く3項目は、文字列の場合の指定です。文字列、文字サイズ、文字色が含まれます。

最後は、ライブラリ内で生成した表示部品に対して、何かの加工を加えるために用意しました。文字列またはUIImageを指定したときも、ライブラリの内部ではUIViewの形で生成します。この生成されたUIViewに対して加工を加えるため、関数のデータ型を「((UIView) -> Void)?」としました。Optional型を選んだのは、nilのときは機能オフにしたいからです。

構造体の中身の後には、初期化の処理を並べてあります。最初の初期化は、画像またはUIViewで使います。関係する項目だけしか含まれてないので、余計なダミー値を指定する必要はありません。2つ目の初期化は、関数を外したバージョンで、関数なしのときに使います。関数なしの場合がほとんどなので、こちらを多く使うでしょう。3つめと4つめの初期化は、文字列の場合に使います。こちらも関数の有無で2つ用意しました。

今まで作ったデータ定義の構造体では、デフォルトで生成される初期化を使っていました。理由は、初期化の処理を書かなくて済むからです。しかし今回は、指定する項目数が多いので、減らす目的で初期化を多く付けてみました。実際に使ってみると、今回のほうが、格段に使いやすいです。過去に作ったライブラリでも、同じような初期化を追加したくなりました。後で追加しようと思っています。

 

続いては、2番目のデータ定義の構造体です。アニメーションの共通情報(時間や繰り返し回数など)を指定するために使います。

// 共通動作定義:各種アニメーションに共通する項目
struct ZavDef1 {
    var sRepeat: Float = 1      // 繰り返し回数
    var sReverse: Bool = false  // アニメ終了後に逆再生
    var sTimeDo: NSTimeInterval // アニメの実行時間(秒)
    var sDelay: NSTimeInterval  // アニメ開始までの遅延(秒)
    var sHid1: Bool = false     // アニメ開始前に隠す
    var sHid2: Bool = false     // アニメ終了後に隠す
    var sFunc1: ((UIView) -> Void)? = nil   // アニメ開始時に実行する処理:処理せずはnil
    var sFunc2: ((UIView) -> Void)? = nil   // アニメ終了時に実行する処理:処理せずはnil
    init(sRepeat:Float, sReverse:Bool, sTimeDo:NSTimeInterval, sDelay:NSTimeInterval, sHid1:Bool, sHid2:Bool, sFunc1:((UIView) -> Void)?, sFunc2:((UIView) -> Void)?) {
        self.sRepeat = sRepeat
        self.sReverse = sReverse
        self.sTimeDo = sTimeDo
        self.sDelay = sDelay
        self.sHid1 = sHid1
        self.sHid2 = sHid2
        self.sFunc1 = sFunc1
        self.sFunc2 = sFunc2
    }
    init(sRepeat:Float, sReverse:Bool, sTimeDo:NSTimeInterval, sDelay:NSTimeInterval, sHid1:Bool, sHid2:Bool) {
        self.sRepeat = sRepeat
        self.sReverse = sReverse
        self.sTimeDo = sTimeDo
        self.sDelay = sDelay
        self.sHid1 = sHid1
        self.sHid2 = sHid2
    }
}

それぞれの項目の役割はコメントとして付けてあるので、簡単に理解できると思います。補足が必要な項目だけ、追加した理由を中心に解説します。

アニメの開始前と終了後に隠す設定は、複雑な動きを実現するために用意しました。それぞれに表示部品に指定できるアニメーションは、回転や異動などを組み合わせても、1種類の動きしか指定できません。もし途中から違う動きをさせたい場合、もっと複雑な指定方法を組み込むことになります。そうせずに、複雑な動きを実現する方法として、アニメーションの前後に消す指定を用意しました。

複数の動きを連続して見せたいときは、次のように指定します。まず最初の動きを指定して、決まった時間だけ動かします。当然、終了時には隠す設定です。この続きは、別なアニメーション指定を追加して実現します。同じ表示部品を使ったアニメーション指定を、終了した時間に開始します。こちらは、開始前に隠す設定です。すると、2つのアニメーションは繋がって再生され、1つの表示部品が、連続した違う動きをしているように見えます。同様に指定すれば、3つ以上の動きを連続して指定できるというわけです。これが、アニメーションの前後に隠す設定を付けた理由です。

最後の項目である2つの関数は、アニメーションの開始と終了時に実行します。表示部品が渡されるので、表示状態を変更することもできます。また、アプリ側の処理を起動するのにも使えます。

 

最後は、3番目のデータ定義の構造体です。2Dアニメーションで指定する情報(位置や回転や透明度など)を入れています。

// アニメ定義:2Dアニメーションの指定項目
struct ZavDef2 {
    var sX1: CGFloat = 0.0      // 位置
    var sX2: CGFloat = 0.0
    var sY1: CGFloat = 0.0
    var sY2: CGFloat = 0.0
    var sZoom1: CGFloat = 1.0   // 拡大縮小
    var sZoom2: CGFloat = 1.0
    var sRoll1: CGFloat = 0.0   // 回転
    var sRoll2: CGFloat = 0.0
    var sOpa1: Float = 1.0      // 透明度
    var sOpa2: Float = 1.0
    var sTF: ZavTF = .ZpLinear  // 動作速度変化
    init(sX1:CGFloat, sX2:CGFloat,sY1:CGFloat, sY2:CGFloat, sZoom1:CGFloat, sZoom2:CGFloat, sRoll1:CGFloat, sRoll2:CGFloat, sOpa1:Float, sOpa2:Float, sTF:ZavTF) {
        self.sX1 = sX1
        self.sX2 = sX2
        self.sY1 = sY1
        self.sY2 = sY2
        self.sZoom1 = sZoom1
        self.sZoom2 = sZoom2
        self.sRoll1 = sRoll1
        self.sRoll2 = sRoll2
        self.sOpa1 = sOpa1
        self.sOpa2 = sOpa2
        self.sTF = sTF
    }
    init(sX1:CGFloat, sX2:CGFloat,sY1:CGFloat, sY2:CGFloat, sTF:ZavTF) {
        self.sX1 = sX1
        self.sX2 = sX2
        self.sY1 = sY1
        self.sY2 = sY2
        self.sTF = sTF
    }
    init(sX1:CGFloat, sX2:CGFloat,sY1:CGFloat, sY2:CGFloat, sZoom1:CGFloat, sZoom2:CGFloat, sTF:ZavTF) {
        self.sX1 = sX1
        self.sX2 = sX2
        self.sY1 = sY1
        self.sY2 = sY2
        self.sZoom1 = sZoom1
        self.sZoom2 = sZoom2
        self.sTF = sTF
    }
    init(sX1:CGFloat, sX2:CGFloat,sY1:CGFloat, sY2:CGFloat, sRoll1:CGFloat, sRoll2:CGFloat, sTF:ZavTF) {
        self.sX1 = sX1
        self.sX2 = sX2
        self.sY1 = sY1
        self.sY2 = sY2
        self.sRoll1 = sRoll1
        self.sRoll2 = sRoll2
        self.sTF = sTF
    }
    init(sX1:CGFloat, sX2:CGFloat,sY1:CGFloat, sY2:CGFloat, sOpa1:Float, sOpa2:Float, sTF:ZavTF) {
        self.sX1 = sX1
        self.sX2 = sX2
        self.sY1 = sY1
        self.sY2 = sY2
        self.sOpa1 = sOpa1
        self.sOpa2 = sOpa2
        self.sTF = sTF
    }
}

表示位置、拡大縮小、回転位置、透明度の4つの項目は、それぞれ開始時と終了時の値を指定します。

ちなみに今回の構造体では、開始が1で終了が2と、変数名を統一してあります。最後に1桁の数字を加える方法なら、変数名を邪魔しない形で、同じ変数名に違いを付けられます。

最後の項目となる動作速度変化だけは、開始と終了という2つの値を持たないので、1桁の数字は付きません。この項目は、2番目の構造体に入れようとも考えたのですが、表示部品の細かな動きを指定する項目なので、3番目の構造体に入れました。

この構造体でも、複数の初期化を用意してあります。単純な動きを指定する際、最小限の項目だけ記述すれば済むようにとの配慮です。まだまだ加えられるので、実際に使ってみて、使う機会の多い組み合わせを、後から追加すると思います。

 

データ定義の構造体を紹介するだけでも、かなり長くなってしまいました。ここで一旦、区切ります。続きは次回の投稿にて。

 

(使用開発ツール:Xcode 6.4, SDK iOS 8.4)

2015年7月27日月曜日

2Dアニメ機能のライブラリを試作(全体設計編)

iOSのAPIには、かなり簡単に使えるアニメーション機能が含まれています。おかげで、いろいろな箇所へアニメーションを付加することが可能です。

それだけにライブラリ化したい重要な候補なのですが、アニメーションが使われる機能や場面が様々で、汎用的なライブラリ化が難しい対象でもあります。

仕方がないので、特定の用途では十分に使えるアニメーション機能として、ライブラリ化してみました。いつものように、UIViewのサブクラスという形でのライブラリ化です。UIViewの中で、指定したアニメーションが動くというわけです。

UIViewであれば、既存のViewに重ねる方法でアニメーションを加えられます。上へ重ねるだけでなく、下に敷いておけば、アニメーションの背景も簡単に実現できるでしょう。

とりあえず、2Dアニメーションの部分だけ試作してみました。どんな形でライブラリ化したのか、どこまでの機能を付けたのか、どれぐらい使いやすいのか、当初期待したほどは良くないのか、いつものように紹介します。

 

まず最初は、大まかな機能を決めます。基本的には、ある程度まで複雑なアニメーションを作れないと、実用にはならないでしょう。そのためには、複数のアニメ部品がそれぞれ独立して動き、全体のアニメーションを構成する形が必須です。

個々のアニメ部品は、それぞれ別々に動きを設定します。使える動きとしては、移動、拡大縮小、回転、透明度変更は必要でしょう。加えて、等速で動くだけでなく、ゆっくり止まるような動きも使いたいはずです。これは、iOSのAPIに含まれるTiming Functionsという指定で可能です。

個々のアニメ部品の動作では、遅延再生や繰り返し再生も必須でしょう。また、再生前と再生後のそれぞれで、部品表示の有無を選べる機能も大事です。これが使えると、いろいろと応用ができますから(この辺の話は使用例で)。

アニメ部品の動きを組み合わせた全体の動作でも、繰り返し再生は必要です。また、一時停止と再生ができると、ユーザーとの対話に役立ちそうです。さらに、キャンセルも必要でしょう。

アニメーションは、動きの指定だけでは実現できません。動かすアニメ部品を用意する必要があります。部品としては、文字列、画像、UIViewの3つを考えました。文字列は、文字列のまま渡すと、文字列の画像が作られて動かせる形です。画像は、画像そのものを動かす形しかないでしょう。UIViewは自由な部品を作れるので、利用範囲を広げるために必須だと考えました。これらをアプリ側が用意して、アニメ機能のライブラリに渡します。

動作の指定は、いつものように定義データとして用意します。その定義データとアニメ部品(文字列、画像、UIView)を組み合わせて、自由なアニメーションを作れるというライブラリを目指しました。

近い将来、3Dアニメーション機能も追加する予定です。そのとき、2Dと3Dを混在させても動くことが必須です。ですから、3D機能を追加しやすいように、また混在しても動くような内部構造で作る必要があります。

 

以上の話をまとめると、次のようになります。

2Dアニメーション機能に求める機能
・UIViewのサブクラスとして作る
・アニメーションの動作は、定義データとして用意する(簡単に使えること)
・定義データ以外には、文字列、画像、UIViewもアニメ部品として使える
・複数の要素を動かせ、それぞれが独立した動きを指定できる
・個々の要素の動きは単純でも構わないが、それを組み合わせて複雑な動きも作れる
・個々の要素の動作として、移動、拡大縮小、回転、透明度を指定できる
・個々の要素の動作として、Timing Functionsを指定できる
・個々の要素の動作として、遅延再生、繰り返しも可能
・個々の要素の動作として、再生前と動作後に、それぞれ非表示も可能
・組み合わせた全体の動作として、繰り返しも可能
・組み合わせた全体の動作として、一時停止と再開、キャンセルも可能
・内部構造では、3D要素を追加しやすく、また2Dと3Dの要素が混在して動くように最初から配慮する

個々のアニメ部品の動きは単純なものばかりですが、それらを組み合わせて使うと、意外に多くのアニメーションを実現できそうです。実際、凝った動きのアニメーションを使う機会はほとんどないですから、これぐらいの機能でも十分だと思います。

 

機能の整理がまとまったので、実現方法の検討に入ります。iOSのAPIに含まれるアニメーション機能は、いろいろな形で用意されているため、どれを使うのか選択しなければなりません。

改めて、いろいろと調べてみたところ、大事な点が分かりました。UIViewから呼び出せるアニメーション機能は、途中でキャンセルできないと判明したのです(もしかしたら間違っているかもしれません。その場合は、お知らせください)。これで、UIViewのアニメーション機能の採用は不可能になりました。

残った候補の中で、一番使いやすいのがCABasicAnimationでした。簡単に使えるものの、機能が意外に多いのが特長です。API上の位置付けとしては、Core Animationを使いやすくするために用意したAPIという感じでしょうか。

UIViewから呼び出せるアニメーション機能よりも、多くの機能があります。将来的な拡張も容易なので、その点でも安心して採用できます。当然ですが、途中のキャンセルも出来ますし、再生の一時停止や再開も簡単です。

 

以上の話をもとに、これから作るクラスの内部構成を作ってみたら、次のようになりました。

2Dアニメーション機能を持つViewクラスの内部構成
・UIViewのサブクラス
・内部データ:定義データ+表示部品、状態保持(再生中、一時停止中)
・初期化(Viewのサイズ、定義データ+表示部品)
・メソッド:
 ・再生開始(繰り返し回数)
 ・一時停止および再開
 ・再生キャンセル
 ・状態問い合わせ(再生中、一時停止中)
・透明ボタン:一時停止と再開
 ・初期状態ではオフ
 ・メソッド:透明ボタンのオンオフ

見て分かるように、機能としては単純です。アニメーション部分だけ作れれば、そのまま完了する感じですね。

アニメーションの再生では、複数の要素が含まれ、それぞれが異なる動作なので、そこさえキッチリと作れれば大丈夫そうです。また、一時停止や再開も、複数の要素を一緒に扱わなければなりません。

実際に作ってみたら、ソースコードは予想外に長くなりました。クラスの本体部分よりも、データ定義のソースが長くなったからです。使い勝手を良くしようと、いろいろな初期化を用意したためです。

 

とりあえず試作したのは、2Dアニメーションだけです。作り終わって一番苦労したと思った点は、アニメーションの処理ではありませんでした。アニメーションの動きを定義する、データ定義の設計に一番苦労したのです。何度か修正を繰り返しながら、やっと落ち着いたという感じでした。

具体的なソースコードは、次回からの投稿で紹介します。

2015年7月18日土曜日

環境設定機能ライブラリもモデルクラス

先日、友人のIT技術者と飲んだとき、このブログの内容に関して、鋭い指摘を受けました。それは、環境設定機能のライブラリも、モデルクラスに含まれるのではないかとの指摘です。

 

言われてみて気付きましたが、確かにそのとおりです。環境設定機能を実現したライブラリは、環境設定機能を汎用化したクラスですが、環境設定機能というモデルでもあります。

ソートや検索といった機能は、必要が無いので持っていません。しかし、定義されたデータ型で値を保持し、アプリとのやりとりを実現しています。また、設定内容をファイルへ保存したり、保存したファイルから環境設定を読み込んだりもできます。まさに、環境設定機能に特化したモデルクラスと言えるでしょう。

 

環境設定機能ライブラリを作ったときは、どのアプリでも簡単に使える環境設定機能を作ろうと考えただけでした。その時点では、モデルクラスまでライブラリ化しようとは考えていません。また、モデルクラスを作り始めたときは、過去に作ったライブラリのどれかが、モデルクラスに該当するなどとは思ってもいませんでした。そのため、友人に指摘されるまで、モデルクラスに該当するとは気付きませんでした。

モデルクラスのライブラリへ環境設定機能が加わったことで、ライブラリは合計3つになりました。また、今回の指摘によって、ライブラリ化するモデルクラスを探す意味で、視野が広がったと思います。どのアプリでも使いそうな機能を洗い出し、それをモデルクラスとしてライブラリ化できそうに感じています。友人の指摘に、大感謝です。

 

環境設定機能ライブラリですが、作ったときのまま改良していません。その後に作った、別なライブラリも増えました。それらを使って、改良できるはずです。

改良点として真っ先に浮かんだのは、ファイルの読み書きを間接参照にするライブラリの利用です。環境設定機能ライブラリでも、ファイルの読み書きを間接参照にして、特定の保存先に依存しない形に改良できます。修正は簡単なので、余裕ができたら実施する予定です。

 

今回の指摘は、一緒に飲んでるときの普通の話として出てきました。でも、このように鋭い指摘をくれる仲間は、本当にありがたいです。お互いに刺激し合いながら、今後も良い関係を続けたいと思います。

2015年7月11日土曜日

UITableViewを簡単に使うライブラリ(機能拡張編)

UITableViewを簡単に使うライブラリの話の続きです。機能を拡張できるように作ってありますが、それとは別な方法での拡張方法を紹介します。

使用例編では、チェックマークを表示させるように拡張した例を取り上げました。また、それを常に使うのなら、もとのライブラリに入れたほうが良いと書きました。その入れ方の話です。

 

ライブラリとして作ったクラスは、UITableViewを素のままで使うのが基本となっています。それ以外の使い方は、用意したカスタマイズ機能を使うのが普通でしょう。

ただし、同じカスタマイズを何度も使うようであれば、カスタマイズ機能で実現するよりも、ライブラリ側に組み込んだほうが、形としてスッキリします。組み込んでしまえば、同じカスタマイズを何度も指定する必要がなくなりますから。

その際に大事なのは、組み込み方です。最初に作ったライブラリのクラスを変更する手もありますが、そのクラスはそのままにしておき、サブクラスの形で組み込むのが賢い選択でしょう。そうすれば、UITableViewを素の状態で使う方法も残りますから。

 

では、実際のSwiftコードを見てみましょう。

// 常に使うようなカスタマイズは、サブクラスの形で組み込む

// サブクラス:カスタマイズ:セルの選択状態を変更:背景を白に変え、代わりにチェックマークを表示する
class ZPTableViewCheckMk: ZPTableView {
    // ======================================== TableViewデリゲート
    // Cellに値を設定
    override func tableView(rTbv:UITableView, cellForRowAtIndexPath rIndexPath:NSIndexPath) -> UITableViewCell {
        let iCell = rTbv.dequeueReusableCellWithIdentifier(ID_CELL, forIndexPath:rIndexPath) as! UITableViewCell
        let iIdx: Int = rIndexPath.row
        iCell.textLabel!.text = cAryTableStr[iIdx]
        iCell.selectionStyle = UITableViewCellSelectionStyle.None  // 追加:セルの背景を白に
        if let iFunc = cFuncEditCell { iFunc(iCell, iIdx) }  // nilでなければ実行
        return iCell
    }
    // Cellが選択されたとき
    override func tableView(rTbv:UITableView, didSelectRowAtIndexPath rIndexPath:NSIndexPath) {
        let iCell: UITableViewCell = rTbv.cellForRowAtIndexPath(rIndexPath)!
        iCell.accessoryType = UITableViewCellAccessoryType.Checkmark  // 追加:チェックマーク付きに
        super.tableView(rTbv, didSelectRowAtIndexPath:rIndexPath)
    }
    // Cellの選択が外れたとき
    override func tableView(rTbv:UITableView, didDeselectRowAtIndexPath rIndexPath:NSIndexPath) {
        let iCell: UITableViewCell = rTbv.cellForRowAtIndexPath(rIndexPath)!
        iCell.accessoryType = UITableViewCellAccessoryType.None   // 追加:チェックマークなしに
        super.tableView(rTbv, didDeselectRowAtIndexPath:rIndexPath)
    }
    // ======================================== テーブル更新
    override func updateTableF(rAryTableStr:[String]) {
        resetAllCellF()   // 追加:残っているチェックマークをクリアーする
        super.updateTableF(rAryTableStr)
    }
    // 追加:すべてのセルをリセット(チェックマークをクリアーする)
    private func resetAllCellF() {
        let iAryCell: [AnyObject] = cTableView.visibleCells()
        (iAryCell as! [UITableViewCell]).map {
            (rCell:UITableViewCell) -> Void in rCell.accessoryType = UITableViewCellAccessoryType.None }
    }
}

見てのとおり、4つのメソッドを上書きしています。使用例のときに関数として追加したコードを、サブクラス内に入れました。コメント部分に「追加」の文字を入れた箇所です。

それぞれのメソッドでは、親クラスのメソッドを呼んで、追加分のコードを書き加えるのが基本です。そうすれば、親クラスとのコード重複がなくなり、メンテしやすいコードになります。しかし、セルに値を設定するメソッド(1番目のメソッド)だけは、親クラスのメソッドを呼んでいません。その理由が一番大事なので、それを先に説明します。

 

カスタマイズ機能を提供する場合は、カスタマイズする人が、全体の動きを理解しやすく作っておかなければなりません。カスタマイズ方法もいろいろあります。カスタマイズ関数を挿入する形の機能であれば、カスタマイズ前の機能がすべて実行された後に、カスタマイズ関数を実行する形が、もっとも理解しやすいくなります。

このことは、そうでない形を考えれば良く分かります。もしカスタマイズ関数の実行後にも何かが実行される形になっていたら、その実行に何が含まれるのか、メソッドごとに意識しなければなりません。カスタマイズする機能によっては、せっかくカスタマイズした内容が、打ち消されてしまうかもしれないからです。

やはり、もとの機能(カスタマイズされる側の機能)がすべて実行されてから、カスマイズの機能が実行されるルールのほうが、一貫性がありますし、理解もしやすいです。これがベストのルールなのです。

実際、親クラスのカスタマイズ関数も、そのように作ってあります。各メソッドの最後に実行する位置に、カスタマイズ関数を挿入しました。

当然ですが、サブクラスとして作るメソッドも、同じように作らなければなりません。各メソッドで、一番最後に実行されるのがカスタマイズ関数という形で。

 

以上の点を意識しながら、サブクラスのメソッドを順番に見ていきましょう。

1番目のメソッドは特別なので、後回しにします。2番目と3番目のメソッド(セルが選択または解除されたときのメソッド)では、カスタマイズ関数が含まれる親のメソッドを、最後に呼んでいます。こうすることでサブクラスでも、カスタマイズ関数が最後に実行されます。当然ですが、このような順序で実行しても(サブクラスに追加したコードを先に実行しても)問題ないことを、親クラスのメソッドを見ながら確認しています。

サブクラスで追加した機能には、セルのインスタンスが必要です。それを得るためのコードも、追加するコードの前に挿入してあります。

続いて、一番最後のメソッド(テーブル更新)です。これには、カスタマイズ関数が含まれません。しかし、セルのチェックマークを先にクリーアする必要があるため、親クラスを呼び出す前に実行してています。また、セルのチェックマークをクリアーする処理を独立させ、メソッドから呼び出す形にしています。ソースコードを見やすくする目的と、もしかしたら別な処理でも使う可能性もあるかなと考えてのことです。

いよいよ、特別扱いした1番目のメソッドです。親クラスのメソッドでは、UITableViewからセルのインスタンスを取得して、そのインスタンスに文字列を設定をしています。親のメソッドの中に、セルのインスタンスを得る処理が含まれているのです。つまり、親のメソッドを実行しない限り、セルのインスタンスが得られないということです。

サブクラスの追加機能では、セルのインスタンスが必要ですから、親のメソッド前に実行できません。親のメソッドを無理して呼ぶ形で作ると、次のようなコード(上側:採用しなかったコード)になります。

// 採用しなかったコード:親クラスのメソッドを呼ぶ形
let iCell:UITableViewCell = super.tableView(rTbv, cellForRowAtIndexPath:rIndexPath) // この中でカスタマイズ関数を実行
iCell.selectionStyle = UITableViewCellSelectionStyle.None  // 追加:セルの背景を白に
return iCell

// 採用したコード:親クラスのメソッドを呼ばない形
let iCell = rTbv.dequeueReusableCellWithIdentifier(ID_CELL, forIndexPath:rIndexPath) as! UITableViewCell
let iIdx: Int = rIndexPath.row
iCell.textLabel!.text = cAryTableStr[iIdx]
iCell.selectionStyle = UITableViewCellSelectionStyle.None  // 追加:セルの背景を白に
if let iFunc = cFuncEditCell { iFunc(iCell, iIdx) }  // nilでなければ実行
return iCell

これでは、カスタマイズ用の関数を最後に実行する形を実現できません。仕方なく、親のメソッドを呼ぶのを諦め、親のメソッドのコードをコピーして、サブクラスのメソッド(下側:採用したコード)を作りました。コードが親クラスと重複してしまいましたが、実現する機能のほうが大事ですから、これしか選択肢はないでしょう。

以上のようなことを考えて作ったのが、今回のコードです。サブクラスが出来上がったら、単体でテストします。拡張した使い方ではなく、クラスのなので、テストしやすいでしょう。

 

続いて、サブクラスの使用例です。いつものように、iOS実験専用アプリから一部のコードを抜き出してきました。次のような感じで使います。

// サブクラスを使う

// 変数を用意する
var tableView14: ZPTableView!   // データ型は親クラスを指定
let aryStr14: [String] = ["acb", "def", "ghi", "jkl", "mno", "pqr", "stu", "vwx"]

// このクラスを使う(テーブルビューを作る)
tableView14 = ZPTableViewCheckMk()              // サブクラスのインスタンス生成
tableView14.setupF(10, 10, 200, 400, aryStr14)  // 初期化(サイズ設定と配列渡し)
tableView14.setFuncSelF(setMsgFromCellF)        // セルを選択した際の関数を設定(下記の関数)
viewBase.addSubview(tableView14)                // アプリ側のビューに貼り付ける

// セルを選択した際に呼び出される関数(アプリ側で用意する)
func setMsgFromCellF(rCell:UITableViewCell, rIdx:Int) {
    vcBase.setMsgF(5, "CellText=" + rCell.textLabel!.text!)  // 選択されたセルの文字列を、アプリ側で表示する
}

見てのとおり、使い方は親クラスと同じです。選択中の表示だけが違い、背景が白くなって、チェックマークが付きます。

カスタマイズ関数が不要になったため、使用例のコードがスッキリしました。テーブルビューを素の状態で使ったときと、同じコードになっています。これが、拡張機能をサブクラスとして組み込んだときの効果です。

今回のサブクラスは、メソッドの構成が親クラスと同じです。そのようなサブクラスを使う際には、変数のデータ型を親クラスにしておき、生成するインスタンスをサブクラスにして代入します。これで問題なく動きます。

変数のデータ型を親クラスにするのは、代入するインスタンスのデータ型を変更したくなったとき、インスタンス生成している箇所のコードだけ変更すれば済むからです。親クラスのインスタンスに変更しても構わないですし、別なサブクラスのインスタンスに切り替えても大丈夫です。

 

以上のように、UITableViewを素の状態ではなく、ある決まったパターンで何度も使う場合は、サブクラスとして拡張するのが良いと思います。

サブクラスとして拡張すれば、拡張のための関数は空のままなので、さらに拡張することも可能です。前回の例のように、最初のカスタマイズを関数として拡張した後、さらに関数へ拡張を追加する場合よりも、見やすく仕上がるでしょう。

 

UITableViewを簡単に使うライブラリの話は、これで一旦終了します。

 

(使用開発ツール:Xcode 6.3.2, SDK iOS 8.3)

2015年7月10日金曜日

UITableViewを簡単に使うライブラリ(使用例編)

UITableViewを簡単に使うライブラリの話の続きです。ライブラリの中身を説明し終わったので、使い方を紹介します。

 

まず最初は、テーブルビューで選択した内容を、アプリ側で受け取る話から。

テーブルビューを使う際には、ユーザーが選択したセルの値を得る手段として、2つの方法があります。1つは、セルを選択した際に処理を実行し、その処理の中で、アプリ側の変数などへ値を設定する方法です。こちらは、セルの選択と同時に、値が設定されます。

今回のクラスで、この方法を実現するためには、セルが選択されたときに実行される関数を用います。アプリ側の変数に値を代入する関数を用意して、関数を設定するメソッドを呼び出すだけです。簡単に実現できます。

もう1つは、アプリ側が値を得るために「登録」などのボタンを用意し、そのボタンがタップされたときに、選ばれている値を取得する方法です。複数のセルを選択できる使い方の場合、こちらのほうが適しています。

今回のクラスで、この方法を実現するためには、選択中の値を取得するメソッドを利用します。アプリ側のボタンがタップされたとき、今回のクラスの取得メソッドを使って、インデックス番号の配列か、セルに表示されている文字列の配列を得ます。

これら2つの使い方は、あくまで基本的な利用方法です。もっと凝った使い方も可能です。たとえば、セルが選択されたときに、選択された画像を表示する必要があるとします。すると、セルを選択したときの関数を用意して、画像表示を実行させるでしょう。このように、選択したセルの値を取得するのとは関係なく、関数を用意して利用する機能を実現する使い方も考えられます。

いろいろな使い方が考えられますが、簡単なものを中心に、いくつかの使用例を挙げてみます。

 

では、最初の使用例を紹介しましょう。最も単純な、巣のままで使う例です。いつものように、iOS実験専用アプリのテストコードから抜き出した例を用いました。

この例では、テーブルビューはそのまま使い、選択されたセルの文字列をアプリ側に表示しています。具体的なSwiftコードは、次のようになります。

// UITableViewに表示する文字列の配列を用意する
let aryStr14: [String] = ["acb", "def", "ghi", "jkl", "mno", "pqr", "stu", "vwx"]

// このクラスを使う
tableView14 = ZPTableView()                     // インスタンス生成
tableView14.setupF(10, 10, 200, 400, aryStr14)  // 初期化(サイズ設定と配列渡し)
tableView14.setFuncSelF(setMsgFromCellF)        // セルを選択した際の関数を設定(下記の関数)
viewBase.addSubview(tableView14)                // アプリ側のビューに貼り付ける

// セルを選択した際に呼び出される関数(アプリ側で用意する)
func setMsgFromCellF(rCell:UITableViewCell, rIdx:Int) {
    vcBase.setMsgF(5, "CellText=" + rCell.textLabel!.text!)  // 選択されたセルの文字列を、アプリ側で表示する
}

使い方や処理内容を理解する前に、ソースコードの短さを見てください。たったこれだけのソースコードで、テーブルビューが表示され、普通に動作するのです。デリゲートを書く必要もありません。テーブルビューに表示する文字列の配列を用意するだけです。しかも、テーブルビューを操作して選択したセルの文字列が、アプリ側でちゃんと表示してくれます。そんな動作も含んでのソースコードです。

こんな短いソースコードで、素の状態という条件付きですが、テーブルビューが使えてしまうのです。ちょっと驚きませんか。これこそが、ライブラリのパワーなのです。

 

本題に戻って、使用例のコードを説明しましょう。

まずは、テーブルビューに表示する文字列の配列です。テスト用のコードなので、配列を手動で用意しました。通常のアプリであれば、何らかの処理の結果として、文字列の配列を生成するはずです。その配列をそのまま使い、テーブルビューに表示することになります。

続いては、テーブルビューを使うクラス(今回のクラス)のインスタンス生成とセットアップです。生成した後で、ビューサイズや配列を指定して初期化しています。必要な関数を設定した後は、アプリ側のビューに貼り付けるだけです。

セルを選択した際の関数では、セルのインスタンス(rCell)と、インデックス番号(rIdx)にアクセスできます。これらの情報を使って、いろいろな機能が実現できるはずです。

 

次の例として、テーブルビューを少しカスタマイズした使い方を取り上げましょう。

素のままのテーブルビューは、選択されたセルの背景色がグレーになるだけです。その背景色を白くして、選択したセルにチェックマークをつける形にカスタマイズします。また、複数のセルを選択可能にしてみます。具体的なSwiftコードは、次のとおりです。

// 別な文字列の配列を用意する
let iAryItems: [String] = ["a", "bb", "ccc", "dddd", "eeeee", "ffffff", "ggggggg", "hhhhhhhh"]

// このクラスを使う(少しカスタマイズした使い方)
tableView14 = ZPTableView()
tableView14.setupF(10, 10, 250, 400, iAryItems)
tableView14.setModeMultiSelectionF()   // 複数選択を可能に設定
// 3つの関数をクロージャとして用意する
let iFuncEditCell: (UITableViewCell, rIdx:Int) -> Void = {   // 背景を白に
    (rCell:UITableViewCell, Int) -> Void in rCell.selectionStyle = UITableViewCellSelectionStyle.None }
let iFuncSel: (UITableViewCell, Int) -> Void = { (rCell:UITableViewCell, rInt:Int) -> Void   // 選択時にチェックマークをつける
    in rCell.accessoryType = UITableViewCellAccessoryType.Checkmark; self.vcBase.setMsgF(5, rCell.textLabel!.text!) }
let iFuncUnsel: (UITableViewCell, Int) -> Void = {   // 選択解除でチェックマークを外す
    (rCell:UITableViewCell, rInt:Int) -> Void in rCell.accessoryType = UITableViewCellAccessoryType.None }
// 3つの関数を設定する
tableView14.setFuncEditCellF(iFuncEditCell)
tableView14.setFuncSelF(iFuncSel)
tableView14.setFuncUnselF(iFuncUnsel)
// アプリ側のビューに貼り付ける
viewBase.addSubview(tableView14)

簡単なカスタマイズを加えましたが、やはり短いソースコードで済みました。

これを動かすと、デフォルトの状態とは表示が異なります。セルを選択したとき、背景色がグレーになりません。その代わりに、選択されたセルには水色のチェックマークが付きます。また、選択を解除すると、チェックマークが消えます。加えて、複数のセルを選択可能なモードになっています。

 

上記の使い方ですが、ここまでの動きには問題ありません。しかし、テーブルビューをリロードしたときに、大きな問題が発生します。

その問題とは、リロード後の表示です。リロードが終わった状態では何も選択されていないはずが、リロード前に選択したチェックマークが、そのまま残っているのです。もちろん、実際には何も選択されていません。ただ表示だけが間違っていて、チェックマークが付いています。

この表示ですが、プログラミングされたとおりに動いた結果なのです。よく考えると、そうなって当然だと理解できます。チェックマークの表示は、セルの選択状態と連動してはいないのです。セルの選択や解除で呼ばれる関数内で、チェックマークを付けたり外したりしています。セルの選択が勝手に変わった場合には、チェックマークを消してくれません。というわけで、当然の動作結果なのです。

こうした状況は、テーブルビューを使う側が解消するしかありません。具体的には、テーブルビューをリロードする前に、すべてのセルでチェックマークを外します。次のようなSwiftコードで。

// テーブルビューの内容を入れ替える(チェックマーク使用時は、事前にお掃除が必要)

// テーブルビューの更新前に、セルをお掃除
let iTvb: UITableView = tableView14.getTableViewObjF()!  // インスタンス取得
let iAryCell: [AnyObject] = iTvb.visibleCells()
(iAryCell as! [UITableViewCell]).map {   // すべてのセルで、チェックマークを外す
    (rCell:UITableViewCell) -> Void in rCell.accessoryType = UITableViewCellAccessoryType.None }
// テーブルビューを、新しい内容に更新
let iAryItemsNew: [String] = ["a001", "b002", "c003", "d004", "e005", "f006", "g007"]
tableView14.updateTableF(iAryItemsNew)  // UITableViewを更新(リロード)

セルのチェックマークを外すために、UITableViewのインスタンスを取得しています。そのインスタンスから全セルの配列を得て、個々のセルでチェックマークを外します。

全セルのチェックマークを外す方法ですが、もっと簡単な方法がありそうだと思い、いろいろと探しました。しかし見付かりませんでした。仕方なく、地道な方法でチェックマークを外しています。今回は、mapメソッドを使ってみました。

セルの掃除が終わったら、セルに表示する新しい配列を用意して、テーブルを更新するメソッドを呼び出します。結果の表示は、各セルの文字列が入れ替わり、何も選択されてない状態に変わっています。

 

上記の例は、言われてみれば当たり前なのに忘れがちな、大事な点を示しています。

ライブラリ自体にバグがないとしても、使い方が悪ければバグが生じてしまいます。そして、関数で拡張するタイプのライブラリは、使い方によるバグが発生しやすいということです。また、クラスのインスタンスをライブラリから受け取って拡張する方法も、同様にバグを作り出しやすいでしょう。

この種のバグを防ぐために、次のような開発手順をお勧めします。まず、ライブラリを拡張して使う際には、ライブラリと拡張した部分だけを作ります。そして拡張込みのライブラリを、1つの機能のようにテストするのです。そうすれば、拡張によるバグを発見しやすくなります。

テストをパスできてから、拡張込みのライブラリをアプリにコピーして利用します。この手順なら、バグが残っている可能性はかなり減らせるはずです。

 

ちょっと横道に逸れますが、mapやfilerといったメソッド(およびグローバル関数)の話を。正直に言うと、機能が少し不足するために使えない場面が多くて、残念に思っています。

まずは、配列のインデックス番号にアクセスできない点です。インデックス番号を返す処理が多く、まったく使えないでいます。抜け道として、enumerate関数を使う方法がありますが、結果がタプルになってしまい、最終結果を配列にしたいことがほとんどなので、これまた使いにくいです。やはり、mapやfilerの巣の状態で、インデックス番号を取得できる手段が必要でしょう。たとえば別な名前、iMapとかiFilterとかで用意する形でも構いません。

もう1つの不満は、mapやfilerが別々になっている点です。両者が合体したメソッドがあれば、利用範囲は格段に広がります。他の言語には存在するので、Swiftでも早急に追加してもらいたいです。Swift 2.0に追加されていると期待したのですが、ありませんでした。

 

本題に戻りましょう。次は、テーブルビューから選択中の要素を得る使い方です。

テーブルビューが使われる状況としては、アプリ側にボタンがあり、そのボタンをタップしたときに取得する形です。また、複数のセルを選択できる状態で使っていることも前提にします。

テーブルビューからの取得は簡単です。それ専用のメソッドが2つあります。インデックス番号の配列を返すメソッドと、セルに表示している文字列の配列を返すメソッドです。インデックス番号を返すメソッドを使うと、次のようになります。

// 追加ボタンをタップしたときに呼ばれる関数
func buttonAddF(rSender:UIButton) {
    // テーブルビューから、選択されたセルのインデックス番号を配列で取得
    let iAryInt: [Int] = tableView14.getSelectionNumF()
    // 取得した番号の数を調べ、ゼロ件ならメッセージを表示
    if iAryInt.count == 0 {
        vcBase.setMsgF(5, "何も選択されていません。")
        return
    }
    // 取得したインデックス番号を使って登録する処理
    for i in 0..<iAryInt.count {
        ...  // 個々のインデックス番号で登録処理
    }
    ...  // 登録の後処理
}

インデックス番号の配列を受け取ったら、最初に件数を調べています。もしゼロ件なら、選択を促すメッセージを表示します。逆に入っている場合は、インデックス番号ごとの処理へ進みます。

この例では、ボタンのタップで呼ばれる関数内に、登録の処理を書いています。しかし実際に作るときは、このような形では作りません。登録処理を独立した関数として作り、ボタンがタップされた時に呼び出させる関数が、登録処理の関数を呼び出す形にします。そうすると、全体がスッキリ整いますし、変更しやすい形になりますから。

 

今回作成したライブラリは、UITableViewを素のままで使うことを大前提としています。もし、チェックマーク付きの形で常に使うのであれば、その設定を加えた形でライブラリを作ったほうが良いでしょう。

使用例では、関数を用意して、チェックマークを付けたり外したりしています。そのコードを、ライブラリの中に組み込めば、ライブラリを呼び出すだけで、チェックマークを使った状態になります。

また、UITableViewをリロードする際には、全部のチェックマークを外す処理が必須です。これも、ライブラリ内のリロードする処理に組み込んでしまえば、問題なく動くでしょう。

以上のように、自分が一番使う設定でライブラリを作り、カスタマイズしないで使う率を増やすのが、上手な作り方となります。カスタマイズ可能に作るのですが、カスタマイズして使うのを極力減らすように作るのが、ライブラリ作りの鉄則と言えるでしょう。

 

単純な例でしたが、今回作ったクラスの使い方を紹介しました。とても簡単に使えることだけは伝わったと思います。

とくに注目したいのが、素の状態で使う場合です。たった数行のコードを書くだけで、テーブルビューが使えてしまいます。デリゲートを書く必要もないどころか、UITableViewクラスの中身を知らなくても使えてしまいます。

さすがにカスタマイズする場合は、UITableViewクラスの中身を知る必要があります。それでも、最初から全部を書くのに比べたら、必要なコード量は格段に減っています。しかも、ライブラリに含まれるコードはテスト済みです。

もう同じようなコードを何度も書く必要はありません。このライブラリを使えば、最低限のコードで、UITableViewが使えます。必要なら、ある程度のカスタマイズも可能です。

今回のクラスを作った後、その直前に作ったクラス(フォトアルバムの中から画像を得るライブラリ)で、さっそく使ってみました。UITableViewを2つ使っているクラスです。使用後はデリゲートが消え、クラス全体がスッキリした形に変わりました。他のライブラリでも、今回のクラスを使って、ソースコードをスッキリさせる予定です。

 

今回のライブラリ化では、デリゲートを含むUI部品であるUITableViewを対象としました。デリゲートを含むUI部品は、他にもあります。同じような考え方でライブラリ化すれば、デリゲートを書かなくて簡単に使えるようになるでしょう。

その際には、少し注意が必要です。UI部品単体での使用に限定せず、一緒に使いそうな機能も含めて、ライブラリ化することです。それもカスタマイズ可能な形で。例えばUITextFieldなら、入力値を検査する機能も一緒に組み込み、カスタマイズできるように作るとかです。そうすれば、ライブラリの価値がさらに高まるでしょう。

 

今回のライブラリを作ったきっかけは、「また同じコードを書くのは面倒だ」と感じたことです。同様に感じる機会は、意外に多いと思います。面倒に感じたときには、ライブラリ化で解消できないか検討したほうが良さそうですね。

 

(使用開発ツール:Xcode 6.3.2, SDK iOS 8.3)

2015年7月9日木曜日

UITableViewを簡単に使うライブラリ(本編)

iOSのAPIには、いろいろなタイプのものが含まれます。その中でも少し面倒なのが、デリゲートを含むAPIです。とくにUITableViewは使用頻度が高く、私が独自開発したライブラリの中でも、何度か使いました。

以前に紹介した、フォトアルバムの中から画像を得るライブラリでも、2つのUITableViewを使っています。それを作った後で、もっと簡単に使えるようにと、UITableViewを使う機能だけのライブラリ化を行いました。UITableViewを超簡単に使えるライブラリとして作ったわけです。凝った使い方をしなければ、たった数行のコードを書くだけで、UITableViewが普通に使えてしまいます。もちろん、デリゲートを書く必要はありません。少し変わったライブラリの例として面白いので、詳しく紹介します。

 

いつものように作り始める前には、大まかな仕様を決定します。とは言うものの、UITableViewを使うだけのライブラリなので、検討する内容はほとんどありませんでした。

大事なのは、デリゲートを書かなくて済む点だけでしょう。すると必然的に、UIViewに貼り付けたUITableViewという形が浮かんできます。つまり、UIViewのサブクラスの形で作り、その中にUITableViewを付ける形です。もっとも簡単な実現方法を選ぶとすると、それしかないと思います。

他に考慮すべき点としては、ある程度の拡張性を持たせることです。UITableViewを使う際、機能を追加しそうな箇所がいくつかあるので、そこに関数を挿入します。その関数を、後から書き換える形にすれば、拡張性は確保できそうです。また、UITableViewのインスタンスをアプリ側で取得できれば、様々な設定変更や機能追加が可能でしょう。この方法でも、拡張性を持たせられます。両方とも採用して、拡張性を高めることにしました。

という感じのことだけ決めて、作り始めました。UITableViewを使った直後だったので、あっさりと仕上がったのを覚えています。いつものように、Swiftコードを挙げながら説明しましょう。

 

まず最初は、入れ物となるクラスからです。UIViewのサブクラスとして作り、UITableViewのデリゲートを作るので、次のような形になりました。

// UITableViewを簡単に使うためのクラス(この中に、後述するコードが入る)
class ZPTableView: UIView, UITableViewDelegate, UITableViewDataSource {
    // クラス内の主要変数
    var cTableView: UITableView!     // TableView
    var cAryTableStr: [String] = []  // TableViewの表示内容
    var cArySelected: [Bool] = []    // 各セルの選択状態
    let ID_CELL: String = "zptvCell"
    // UITableViewデリゲートの中で呼ばれる関数
    var cFuncEditCell: ((UITableViewCell) -> Void)? = nil
    var cFuncSelectCell: ((UITableViewCell, Int) -> Void)? = nil
    var cFuncUnselectCell: ((UITableViewCell, Int) -> Void)? = nil
}

クラス宣言の部分では、UIViewのサブクラスであることと、UITableViewのデリゲートなどを指定しています。

クラス内に用意した変数は、UITableViewに加えて、セル内に表示する文字列の配列、各セルの選択状態を記録するBool型の配列、セルのIDとなる文字列です。

さらに、デリゲートの中で呼ばれる関数も3つ用意しました。より多く用意できるのですが、最低限のカスタマイズを可能にする形として、3つの箇所に関数を挿入しています。どの関数もOptional型で、初期値にはnilを入れてあります。なぜOptional型にしたのかは、後で説明します。

 

続いて、クラスの初期化です。いつものように、initとは別な名前で用意しています。

// ======================================== 初期化
// UIViewの最小サイズ
let VIEW_WIDTH_MIN: CGFloat = 100
let VIEW_HEIGHT_MIN: CGFloat = 350

func setupF(rX:CGFloat, _ rY:CGFloat, _ rWidth:CGFloat, _ rHeight:CGFloat, _ rAryTableStr:[String]) {
    if (rWidth < VIEW_WIDTH_MIN) { zSendErrMsgF("ERROR:ZPTV_STUP:Viewの幅が不足"); return }
    if (rHeight < VIEW_HEIGHT_MIN) { zSendErrMsgF("ERROR:ZPTV_STUP:Viewの高さが不足"); return }
    self.frame = CGRectMake(rX, rY, rWidth, rHeight)
    // その他の処理
    cAryTableStr = rAryTableStr    // テーブル表示内容の設定
    setupTblViewF()                // TableViewの生成
    setupEtcF()                    // その他
}
// ======================================== TableView生成
private func setupTblViewF() {
    // TableViewの生成:UIViewと同じ位置とサイズで生成する
    let iX: CGFloat = 0
    let iY: CGFloat = 0
    let iWidth: CGFloat = self.frame.size.width
    let iHeight: CGFloat = self.frame.size.height
    cTableView = zCreateTblViewF(iX, iY, iWidth, iHeight)
    cTableView.registerClass(UITableViewCell.self, forCellReuseIdentifier:ID_CELL)
    cTableView.delegate = self
    cTableView.dataSource = self
    self.addSubview(cTableView)
}
// ======================================== その他の初期化
private func setupEtcF() {
    // TableViewに表示する文字列配列と同じ要素数の、false配列を作る
    cArySelected = [Bool](count:cAryTableStr.count, repeatedValue:false)
}

短いので3つに分ける必要はないのですが、長さに関係なく、このようなスタイルで作るのが癖になっています。3つ目の処理だけは、別な処理でも呼ばれるので分けてあります。

初期化の最初では、ビューの大きさを検査しています。最小値より小さければ、エラーメッセージを出して終了します。単純な指定ミスを発見するための検査です。エラー検査を通ると、ビューの位置と大きさを設定して、テーブルビューの文字列も保存します。

続いて、テーブルビューを生成します。テーブルビューの大きさは、クラス自体のビューと同一です。テーブルビューの生成関数を呼び出して生成し、最低限の属性設定も行います。最後に、クラスのビューに追加して終わりです。

その他の初期化では、各セルが選択中かを保存する配列を、初期化しています。同じ値を入れるだけなので、配列に備わっている初期化機能を使いました。

 

テーブルビューができたので、次はデリゲート関係です。

// ======================================== TableViewデリゲート
// Cellの総数を返す
func tableView(rTbvName:UITableView, numberOfRowsInSection rSection:Int) -> Int {
    return cAryTableStr.count
}
// Cellに値を設定
func tableView(rTbvName:UITableView, cellForRowAtIndexPath rIndexPath:NSIndexPath) -> UITableViewCell {
    let iCell = rTbvName.dequeueReusableCellWithIdentifier(ID_CELL, forIndexPath:rIndexPath) as! UITableViewCell
    let iIdx: Int = rIndexPath.row
    iCell.textLabel!.text = cAryTableStr[iIdx]
    if let iFunc = cFuncEditCell { iFunc(iCell, iIdx) }       // nilでなければ実行
    return iCell
}
// Cellが選択されたとき
func tableView(rTbvName:UITableView, didSelectRowAtIndexPath rIndexPath:NSIndexPath) {
    let iCell: UITableViewCell = rTbvName.cellForRowAtIndexPath(rIndexPath)!
    let iIdx: Int = rIndexPath.row
    cArySelected[iIdx] = true // 選択された
    if let iFunc = cFuncSelectCell { iFunc(iCell, iIdx) }    // nilでなければ実行
}
// Cellの選択が外れたとき
func tableView(rTbvName:UITableView, didDeselectRowAtIndexPath rIndexPath:NSIndexPath) {
    let iCell: UITableViewCell = rTbvName.cellForRowAtIndexPath(rIndexPath)!
    let iIdx: Int = rIndexPath.row
    cArySelected[iIdx] = false // 選択が解除された
    if let iFunc = cFuncUnselectCell { iFunc(iCell, iIdx) }  // nilでなければ実行
}

どの処理も、テーブルビューを使う際の最低限のことしか行っていません。テーブルビューを素の状態で使うために必要なことばかりです。

ただし2点だけ、機能を追加しています。その1つが、各セルの選択状態を記録する機能です。選択されたらtrueに、選択が解除されたらfalseに設定しています。

もう1つの追加機能は、関数の挿入です。ただ関数を実行する形ではなく、条件によって実行する形です。具体的には、関数を入れる変数の値がnilなら実行せず、変数に処理が入っていれば実行しています。つまり、関数用の変数にnilを入れることで、関数の機能をオフできる形なのです。このように動作さセルために、関数用の変数をOptional型にしました。

これとは逆に、関数用の変数をOptional型にしない場合は、何もしない関数を入れておく必要があります。そして、何もしない関数が毎回実行されることにもなります。そのような動作を嫌って、関数変数をOptional型にしたわけです。何もさせたくないなら、ただnilを入れておけば、その判定だけしか実行されないはずです。

また、このクラスを使う側にも良い点があります。一度設定した関数をオフしたい場合、関数をnilに設定すれば済みます。何もしない関数を用意する必要はありません。そのように使う機会は少ないと思いますが。

実は、このライブラリを作った時点では、Optional型の変数を使わずに、何もしない関数をデフォルトで入れていました。別なライブラリを作った際に、変数をOptional型で作れば良いと気付きました。そして、このライブラリも同様に修正したという流れです。いろいろなライブラリを作っていると、より良い方法が少しずつ見付かります。そして、別なライブラリも同様の形に修正するというのが、よくあるパターンですね。

 

セルが選択されている状態は、クラス内にBool型の配列を用意して保存しています。そのため、セルが選択されたときと解除されたときに、この配列の値を更新しています。少し面倒です。

その代わりのメリットとして、セルの選択状態を調べたいとき、個々のセルにアクセスしなくて済みます。当然ですが、セルへアクセスする処理では、iOSのAPIを使います。もし使っているAPIの仕様が変われば、アクセスする処理も変更を求められます。

こんな点まで考慮すると、Bool型の配列を使う良さが理解できます。APIを使わないことで、APIの仕様変更による修正を、少しでも減らす意味があるわけです。もちろん、Bool型の配列も、何かの仕様変更で変更になる可能性はあります。ただ、今回のようなコードでは、可能性がかなり低いでしょうし、あったとしても面倒な変更にはならないでしょう。

単純な機能ですが、こんな感じで考えて作りました。私の場合は、少し面倒な方法であっても、APIにアクセスするコードを減らす方針でプログラミングしています。

 

デリゲートに挿入した関数を、設定するためのメソッドも必要です。関数が3つあるので、メソッドも3つ用意しました。それぞれの関数はOptional型なので、nilを設定することもできます。

// ======================================== 関数の設定
// セルに値を設定するときに呼ばれる関数
func setFuncEditCellF(rFunc:((UITableViewCell, Int) -> Void)?) {
    cFuncEditCell = rFunc
}
// セルが選択されたときに呼ばれる関数
func setFuncSelF(rFunc:((UITableViewCell, Int) -> Void)?) {
    cFuncSelectCell = rFunc
}
// セルの選択が解除されたときに呼ばれる関数
func setFuncUnselF(rFunc:((UITableViewCell, Int) -> Void)?) {
    cFuncUnselectCell = rFunc
}

3つの関数はデータ型が同じなので、1つの関数にまとめて、配列で渡す方法も考えました。しかし、このクラスを拡張する場合、おそらく関数が増えるでしょう。その際、関数のデータ型が違うと、まとめるのが面倒になります。メソッドを最初から3つに分けて、拡張時に関数が追加されたときも、各関数の同じ位置付けになるようにしておいたほうが良いと判断しました。

 

テーブルビューのデフォルトは、1つのセルだけ選択できるモードです。しかし設定を変更すると、複数を同時に選択するモードでも使えます。このようなモード変更は、使用頻度が高いので、専用のメソッドを用意しました。

// ======================================== 選択可能数のモード変更
func setModeSingleSelectionF() {
    cTableView.allowsMultipleSelection = false   // 初期状態では、こちらになっている
}
func setModeMultiSelectionF() {
    cTableView.allowsMultipleSelection = true
}

処理内容としては、UITableViewのプロパティを変更しているだけです。このようなメソッドを用意すれば、長いプロパティ名を調べる手間が省けるでしょう。

このメソッドの一般的な使い方は、このクラスのインスタンスを生成した後、すぐに実行するものです。けっして、途中で切り替えるものではありません。また、デフォルトのままで構わない場合は、おそらく使うことはないでしょう。

 

ライブラリとして作るわけですから、取得関係のメソッドも何種類か用意しました。大きく分けて、以下の3種類です。

// ======================================== TableViewの取得
func getTableViewObjF() -> UITableView? {
    return cTableView
}
// ======================================== 全要素の取得
func getCellCountF() -> Int {     // 要素数を返す
    return cAryTableStr.count
}
func getAllStrF() -> [String] {   // 全セルの文字列を、配列で返す
    return cAryTableStr
}
// ======================================== 選択された要素の取得
func getSelectionNumF() -> [Int] {  // 選択されたセルのインデックス番号を、配列で返す
    var iAryInt: [Int] = []
    for i in 0..<cArySelected.count {
        if cArySelected[i] == true {
            iAryInt.append(i)
        }
    }
    return iAryInt
}
func getSelectionStrF() -> [String] {  // 選択されたセルの文字列を、配列で返す
    var iAryStr: [String] = []
    for i in 0..<cArySelected.count {
        if cArySelected[i] == true {
            iAryStr.append(cAryTableStr[i])
        }
    }
    return iAryStr
}

最初のメソッドは、UITableView自体のインスタンスを返します。初期化が失敗するとインスタンスが生成されていませんから、戻り値をOptional型にしてあります。Optional型にしないと、このクラス自身がクラッシュしますので。

UITableViewのインスタンスを返すメソッドには、他のメソッドとは根本的に異なる、大きな役割があります。アプリ側は、UITableViewのインスタンスを得られることで、UITableViewに対する様々な設定変更が可能になります。おかげで、このクラスが用意していない機能や設定も、ある程度までですが、実現できる手段が得られます。このようなメソッドを用意することで、クラスの機能不足を補うことが可能となります。そんなわけで、かなり重要なメソッドなのです。

全要素の取得は2つのメソッドです。1つめのメソッドは、インデックス番号の配列を返しても構わないのですが、ゼロから始まる連続数になるだけです。それは無駄なので、要素数を返す形にしました。セルの文字列を返すほうのメソッドは、文字列の配列を返しています。

選択された要素を返すメソッドも2つです。インデックス番号を返すメソッドでは、番号の配列を返します。セルの文字列を返すメソッドでは、文字列の配列となります。どちらのメソッドも、1つだけ選択するモードと、複数を選択するモードの両方に対応しています。1つだけ選択するモードでは、空の配列か、要素が1つだけの配列を返します。

 

最後は、テーブルを更新するメソッドです。セルに表示する文字列の配列を受け取って、テーブルビューをリセットします。

// ======================================== テーブル更新
func updateTableF(rAryTableStr:[String]) {
    cAryTableStr = rAryTableStr
    cTableView.reloadData()
    setupEtcF()     // その他の初期化:全セルの状態記録を非選択に
}

処理内容としては、まず文字列の配列をクラス内に保存します。続いて、そのデータを、テーブルビューにリロードさせます。

この状態では、セルが何も選択されていません。セルの選択状態を保存しているBool型の配列を、初期化する必要があります。クラスを初期化する際に使った関数を呼び出して、初期化しています。

 

以上で、今回のクラスに含まれるソースコードを全部紹介しました。このライブラリのおかげで、テーブルビューがどれだけ簡単に使えるのか、それこそが最大のポイントでしょう。その話に関係する使用例は、次回の投稿にて。

 

(使用開発ツール:Xcode 6.3.2, SDK iOS 8.3)

2015年7月7日火曜日

ライブラリ化の当面のゴール

このブログで紹介しているように、Swiftで書いた独自ライブラリを少しずつ作り進めています。まだまだ途中の段階ですが、どんなゴールを描いているのか、簡単に紹介したいと思います。

最初に言っておきますが、これから書く話は、ゲームなどのアプリを作るのが前提ではありません。あくまで業務アプリなどの、遊び要素の少ないアプリの開発に関しての話です。ゲームのアプリなら、大規模な開発ツールが既に作られていて、それを使うのが賢い選択でしょうから。

 

新しいOSや開発ツールを使い始めたとき、独自のライブラリ化を進めるのは、開発経験が長い人なら一般的でしょう。ただ、どのようにライブラリ化するのか、どんなゴールを目指して進めるのかは、人によって異なると思います。

私の場合は、開発やメンテの手間を減らすことです。それにより、開発期間が短くなりますし、完成後のメンテの負荷も小さくなります。とにかく幅広く使えるライブラリを増やして、アプリ開発の手間を大きく減らすことが目的です。

ライブラリとして作ったコードは、単独で十分なテストをされているのに加えて、複数のアプリで使われているため、品質は高く保たれがちです。また、OSやツールのバージョンアップで変更が生じたときも、ライブラリ単体で更新してテストを済ませれば、あとはアプリ側のライブラリを入れ替えるだけです。複数アプリを合計した際の作業量は、かなり減るでしょう。

あと、ライブラリの形式として、ソースコードのままで使う方法を重視しています。ソースコードのままアプリに追加する形なので、ライブラリにバグがあっても簡単に見付かりますし、ライブラリを拡張したくなったときも、可能性が簡単に判断できます。ソースコードのまま追加するものの、基本的にはライブラリに手を加えません。ただし、どうしても手を加えなければならないケースでは、ソースの先頭に変更内容を明記する形で、認めてはいます(実際には、めったにやりません)。当然ながら、ライブラリを改良して対応するのが、第一の選択肢です。

以上は、あくまで、目先の目的としての話です(もっと大きな目的は、最後のほうに出てきます)。

 

いろいろなアプリを作っていると、似たようなコードを数多く書いている自分に気付きます。たいていは1から書かず、前に作ったソースコードをコピーしてから修正するでしょう。コピーによって手間は減っていますが、似たようなコードを書いている現状には変わりません。

そんな風に考えていると、似たようなコードを書かなくて済む方法はないか、深く考えるようになります。部分的にコピーして作るのではなく、ソースコード全体をファイル単位でアプリに持って行き、そのままで使える状態に作れないかと。これがライブラリ化の始まりです。おそらく、独自のライブラリを作っている人は、似たような経験や考えがあるはずです。

実際にライブラリ化を進めると、かなりの効果があると実感できます。似たようなコードを書くことが減り、ライブラリで済ませられる範囲が少しずつ増えていきます。もし既存のライブラリで機能が不足する場合も、ライブラリのほうを拡張して、新しいアプリに対応させます。その結果、ライブラリが適用できる範囲が少しずつ増えていきます。

Swiftを使い始めてから、それほど時間が経過していません。1.0の正式版が登場してからですから、まだ1年未満です。それでも、ライブラリを少しずつ作り続けたので、だんだん充実してきました。モデルクラスのライブラリも2つ作り、ライブラリの対象範囲も広がっています。今後の充実が、ますます楽しみです。

 

続いて、別な視点でライブラリを考えてみます。ライブラリ化する機能の種類で考えると、ライブラリ化のゴールが見えやすくなります。

真っ先に作ったのは、どのアプリでも必ず使う機能でした。UI部品の生成、日付と時間、ファイルの閲覧、電子メールの送信、描画機能などです。それらが作り終わると、環境設定など、どのアプリでも作りそうな機能をライブラリ化しました。こうした種類のライブラリ化は今も続けていて、ライブラリによるサポート範囲をさらに広げようとしています。

アプリ独自の要素として作らなければならない機能は、扱うデータを操作するモデルクラス、それを画面に表示して操作するビューとコントローラのクラスでしょう。作る量が大きい順に並べると、ビュー、モデル、コントローラとなります。

このうち、モデルのクラスは、すでにライブラリ化を進めています。まだ種類は少ないですが、少しずつ種類を増やして、ライブラリで済ませる率を向上させる予定です。

ビューは、モデルのように、個々のモデル全体をライブラリで作る形が難しい分野です。ビューの一部をライブラリとして用意し、それらを組み合わせて1つのビューを実現します。つまり、部品として作るのが適している分野です。ただし、機能の種類が多くあるため、ライブラリでカバーできる比率は、なかなか上がらないでしょう。

当面の方向性としては、次のように考えています。ビューを実現するための要素を、幾つかのタイプに分類して、タイプごとにライブラリを増やすつもりです。今までは、部品となる小さなビューだけでした。しかし、モーダルビューのように、動きの実現がメインとなるライブラリも、今後は増えていくでしょう。また、ビュー上にUI部品を繰り返して並べるような機能も、ライブラリとして作りました。これなどは、配置機能に特化したライブラリです。さらには、いろいろなUI部品を生成する関数群、同じUI部品を一気に生成する関数群など、ビューの構成部品の生成もライブラリ化しています。

あと、紹介していませんが、環境設定の専用ビューもライブラリを試作しました。以前に紹介した、環境設定の機能をライブラリ化したものと連動して、環境設定の値を変更するためのビューを生成します。凝った機能には対応できないレベルですが、一般的な設定の範囲であれば、もう使える状態です。以前紹介した環境設定のライブラリを使うと、環境設定の値を定義するだけで、環境設定の機能が作れます。ビューも同様で、環境設定画面の定義を用意するだけで、環境設定を変更するビューが作れてしまいます。これら2つは連動するので、環境設定機能とビュー機能のやり取りは、コーディングする必要がありません。このように、特定の機能を実現するビューなら、ライブラリとして、まだまだ作れる範囲があると思います。

 

アプリ全体として見た場合、ライブラリが適用できなくて、そのアプリ専用にコーディングが必要となる領域は、ビュー関係に一番多く含まれます。つまり、ライブラリ化の一番の肝は、ビューに関する機能のライブラリ化をどれだけ実現できるかです。モデルに関しては、5種類ほど作れれば、半分以上のモデルをライブラリで作れるでしょう。ビューでは、そのような考え方が通用しません。どんな切り口でライブラリ化したら良いのか、どうすれば新しい工夫ができるかが、ビューに関するライブラリ化の度合いを決めます。

今後は、さらにレベルアップできないか検討中です。もう少し違った切り分け方で、ビューの機能を実現できないかと、かなり考えています。まだ結論は出ていないものの、もしかすれば何とかなりそうなヒントだけは思い浮かびました。これを発展させて、新しい種類のライブラリが作れたら、ビューに含まれる機能のライブラリ比率が、かなり向上させられそうです。

 

アプリとして作る機能のうち、モデルとビューでのライブラリ比率が向上できると、アプリ全体でのライブラリ化率がかなり向上できます。

ライブラリなしで開発したときのコード量を10割としたとき、ライブラリの使用により何割減らせるかが、ライブラリの効果を評価する際の数値になると考えています。ライブラリなしで開発したときのコード量というのも、やや大雑把ですね。あえて説明すると、すべての機能をベタで書いたとき(雑誌やウェブでのサンプルのようなコード)のコード量という意味です。

Swiftのライブラリ化を始めた頃は、どの程度まで作れるのか予想が付きませんでした。漠然とですが、遠い将来は5割が目標だけど、当面は3割ぐらいかなと考えていました。

ところが、ライブラリが増えるごとに、それもライブラリの対象範囲が広がるとともに、5割は簡単に実現できると思うようになりました。実際、今まで作ったライブラリの機能だけに限定したアプリを作るなら、5割ぐらいはライブラリで済ませられ、残りの5割をコーディングする形になりそうです。

もし新しい機能を使うアプリを開発する場合は、その新しい機能の部分も、ライブラリとして作ります。そうすると、ライブラリが対応できる機能が増え、似たようなアプリを再び開発する場合は、やはり5割ぐらいがライブラリで対応できる状態になります。

今では、将来的に7割ぐらいも可能じゃないのかと、思い始めています。前述のビュー機能のライブラリ化が、どこまで実現できるかにかかっています。ただし、ライブラリがいくら充実しても、7割強ぐらいが限界ではないかと思います。ライブラリとして作りにくい機能が、ある程度は残りますから。

 

以上の話は、コード量に関してだけです。アプリ開発では、コードを作る以外の作業が多く含まれます。それが減らない限り、開発の負荷は大幅に減りません。その部分に関しては、かなり難しいでしょう。

それとは別に、開発スタイルが大きく変わる可能性はあります。新規に作らなければならないコードの開発量が減ると、とりあえず試しては変更し、異なる仕様を何度も試せます。ライブラリで実現した機能は、使う際の定義データや使い方を変更するだけで、ある程度まで機能を変えられます。もし機能が不足する場合は、ライブラリを拡張して、新しい機能を追加するでしょう。どちらにしても、いろいろと変えながら試せる可能性は、今よりもずっと高まります。

このような開発スタイルが、ライブラリ化を進めている当面のゴールです。それを実現するために、ライブラリで作るコードの割合を7割ぐらいにするのが、別な視点での当面のゴールとなります。

ライブラリを使って作ったアプリは、俗に言うプロトタイプではありません。本番でも使える、実際に動くアプリです。本番用のアプリを、プロトタイプ的に作ろうというのが、ライブラリ化の本当の目的と言えるかもしれません。

 

今回は趣向を変えて、ライプラリが充実したときの状態を、当面のゴールという形で書いてみました。具体的なコードばかり見ていると、大きな目標を見失ったりします。せっかくですから、大きな目標を掲げて、それに向かって努力したほうが、良い結果が得られるでしょう。これを読んだのを良い機会にして、自分なりの当面のゴールを、考えてみてはいかがでしょうか。

2015年7月2日木曜日

共通モデルのライブラリ化を試作(機能追加編2)

共通で使えるモデルクラスをライブラリ化する第1段として、記録カードのモデルクラスを作る話を紹介しました。前回の投稿で書いたとおり、内部配列を変更しました。その後に、新しい機能を追加したので、また紹介します。

 

今回追加したのは、ソート機能。説明する必要のないでしょうが、指定された項目値の大きさで、レコードを並び替える機能です。

最初に決めなければならないのは、ソート機能の組み込み方でしょう。ソートしたデータをどのように利用するのか、いろいろと考えました。ソート結果をモデルクラス内で持ち、順番に取り出す方法も考えられます。しかし、そうするとソート以外の機能を用意しなければならず、作る量が増えてしまいます。また、用意した機能で不足する状況も考えられます。

それを避けるために、ソート後のレコード番号を配列の形で渡すことにしました。レコード番号さえあれば、あとは自由に使えます。レコード番号を指定して、レコード内の項目値が得られますし。

ソートには昇順と降順があります。昇順の配列は、逆から読むと降順として使えます。そのため、昇順のソートだけを作ることにしました。

 

実行に必要なのは、どの項目でソートするかの指定だけですから、引数は項目番号だけで済みます。また、戻り値はレコード番号の配列です。データ型が違っても、まったく同じです。というわけで、メソッドは1つだけになりました。

ただし、Bool型だけは値が2種類しかないので、ソートする意味がありません。無駄な処理をさせないために、エラーメッセージを出して終了することにしました。

では、具体的なコードを見ましょう。次のようなSwiftコードを追加しました。

// ======================================== ソート:レコード番号の配列を返す
func sortF(rFldIdx:Int) -> [Int] {
    if cAryType[rFldIdx] == .ZmBool { zSendErrMsgF("ERROR:ZMR_SORT:項目定義のデータ型がBoolでは使用不可"); return [Int]() }

    // Step1:指定された項目値とレコード番号のペア配列を要素とする、レコード全体の2次元配列を作る
    var iAryAnySort: [[AnyObject]] = []  // ソートで使う2次元配列:データは[[項目値, RecIdx], ...]の形になる
    for i in 0..<cRecCount {   // i=RecIdx
        var iAnyPair: [AnyObject] = []        // 項目値とRecIdxのペアを入れる配列
        iAnyPair.append(cAryDataAny[rFldIdx][i])  // 指定された項目の値
        iAnyPair.append(i)                        // RecIdx
        iAryAnySort.append(iAnyPair)          // ペア配列をソート用配列に追加
    }

    // Step2:指定された項目値で、2次元配列をソートする:ペア配列の最初の要素を取り出して、大小を比較する
    // ペア配列(a0とa1)の最初の要素(a0[0]とa1[0])を取り出して、大小を比較する
    switch cAryType[rFldIdx] {
    case .ZmInt:
        iAryAnySort.sort { (a0:[AnyObject], a1:[AnyObject]) -> Bool in return ((a0[0] as! Int) < (a1[0] as! Int)) }
    case .ZmFloat:
        iAryAnySort.sort { (a0:[AnyObject], a1:[AnyObject]) -> Bool in return ((a0[0] as! Float) < (a1[0] as! Float)) }
    case .ZmStr:
        // 英字の大文字小文字に加え、全角英字の大文字小文字の違いを、すべて無視してくれるソート
        iAryAnySort.sort { (a0:[AnyObject], a1:[AnyObject]) -> Bool in return
            (a0[0] as! String).localizedCaseInsensitiveCompare(a1[0] as! String) == NSComparisonResult.OrderedAscending }
    default:
        zSendErrMsgF("ERROR:ZMR_SORT:ここは通らないはず:実行されたらバグあり")
        return [Int]()
    }

    // Step3:2次元配列から、レコード番号だけの配列を作る
    var iAryRecIdx: [Int] = []
    for i in 0..<cRecCount {   // i=RecIdx
        let iRexIdx: Int = iAryAnySort[i][1] as! Int  // ソートされたRecIdxを取り出す
        iAryRecIdx.append(iRexIdx)
    }
    return iAryRecIdx
}

いつもより長めのコードで、全体が3つの処理に分かれているので、Step1〜3の表記をコメントにつけています。これで、処理の区切りが明確に伝わります。

3つの処理の前に、指定された項目がBool型かを検査します。該当する場合は、エラーメッセージを出して空の配列を返します。空の配列を返すのは、戻り値をOptional型にしたくないからです。

 

3つのステップを順番に見ていきましょう。

まずは、ステップ1です。1つの配列をソートするだけでは、並び替えたレコード番号が得られません。また、項目値を入れたAnyObject配列には、レコード番号が入っていません。

レコード番号と一緒にソートできる配列を、新しく用意する必要があります。ソートする項目値とレコード番号をペアにして配列に入れ、それをレコード数の分だけ並べます。その集まりを、ペアの中の項目値でソートすると、レコード番号も一緒に並び替えられ、ソート後のレコード番号が得られるというわけです。

つまり、2次元の配列を用意する必要があります。このような配列を作るために、レコード数だけforループさせ、ペアとなる配列iAnyPairを毎回作って、ソート用の配列iAryAnySortへ次々と入れていきます。

続いて、ステップ2です。配列iAryAnySorが出来上がったら、その配列自体をソートします。sortメソッドを使い、値の大きさを判定するための関数(クロージャ)を渡すだけです。ここでは、データ型を指定する必要があるので、項目のデータ型ごとに処理を分けています。

このクロージャの作り方は、少し注意しなければなりません。ソートする配列は、2次元のAnyObject配列です。ですからsortメソッドが取り出して比較するのは、1次元のAnyObject配列になります。そのままだと比較できないので、1次元配列の最初の要素(項目値)を取り出し、Int型にキャストして比べる必要があります。そのため、(a0[0] as! Int)や(a1[0] as! Int)という形になっているわけです。また、引数のデータ型も、ちゃんと[AnyObject]を指定しています。

このsortメソッドが実行されると、2次元配列iAryAnySort内で、ペア配列が項目値の順に並べ替えられます。あとは、順番に取り出すだけです。

ステップ3の取り出しの処理では、戻り値となるレコード番号の配列を作りながら、レコード数の回数だけforループを実行します。ループが終了したら、レコード番号の配列を返して終了です。

 

今回のメソッドの中で一番理解しづらいのが、ステップ2で使っているsortメソッドのクロージャでしょう。普通の配列をソートするのではなく、2次元配列をソートしていますから。

処理内容が少しでも理解しやすいようにと、クロージャはかなり親切に書いています。何も省略しない書き方なので、これが精一杯でしょう。あとはコメントで補足するしかありません。コメントも、短いながら丁寧に書きました。

// 省略しない書き方
iAryAnySort.sort { (a0:[AnyObject], a1:[AnyObject]) -> Bool in return ((a0[0] as! Int) < (a1[0] as! Int)) }

// 省略した書き方
iAryAnySort.sort { ($0[0] as! Int) < ($1[0] as! Int) }

逆に省略した書き方も可能です。でも今回のような使い方では、クロージャ内の引数のデータ型が書かれていないと、前の処理を見ながらデータ型を調べる必要があります。結果として、何をしているのか理解しづらくなります。

少し変わった形でソートする場合は、sortメソッドに渡すクロージャを、できるだけ理解しやすく(省略なしで)書いたほうが良いと思います。もちろん、コメントでの補足も忘れずに。

 

ステップ2のソートの処理では、String型だけが違う書き方になっています。

他のデータ型のような不等号だけでは、期待したようにソートされず、使いものになりません。英字には大文字と小文字があり、それらを区別してソートしてしまうからです。さらに、日本語にも全角の英字が含まれ、それにも大文字と小文字があります。これらも区別してソートしてしまいます。数字の全角と半角も同様に、区別してソートしてしまいます。

望まれているソートの形は、もっと違うものです。半角英字の大文字と小文字、全角英字の大文字と小文字、それら4つの種類を区別しないで、同じ英字として処理してくれるソートなのです。

このような目的のために、ローカライズされた文字比較の機能が用意されていて、String型で使えます。具体的にはlocalizedCaseInsensitiveCompareメソッドで、比較したい文字列を渡し、判定結果がNSComparisonResult型で返ってきます。

2種類の比較方法を、あえて並べて書くと、次のようになります。

// 英字の大文字小文字に加え、全角英字の大文字小文字の違いを、すべて区別するソート
iAryAnySort.sort { (a0:[AnyObject], a1:[AnyObject]) -> Bool in return ((a0[0] as! String) < (a1[0] as! String)) }

// 英字の大文字小文字に加え、全角英字の大文字小文字の違いを、すべて無視してくれるソート
iAryAnySort.sort { (a0:[AnyObject], a1:[AnyObject]) -> Bool in return
    (a0[0] as! String).localizedCaseInsensitiveCompare(a1[0] as! String) == NSComparisonResult.OrderedAscending }

今回のソート機能では、下側のほうを使いました。これで、英字の大文字や小文字が含まれていても、全角の英字や数字が含まれていても、アルファベット順や数字順で正しくソートしてくれます。

 

String用ソートでも、できるだけ理解しやすいようにと、省略しない形で記述しています。逆に、少し短くした記述方法も可能です。今回の場合は、あまり短くなりませんが。

// 省略しない書き方
iAryAnySort.sort { (a0:[AnyObject], a1:[AnyObject]) -> Bool in return
    (a0[0] as! String).localizedCaseInsensitiveCompare(a1[0] as! String) == NSComparisonResult.OrderedAscending }

// 省略した書き方
iAryAnySort.sort { ($0[0] as! String).localizedCaseInsensitiveCompare($1[0] as! String) == NSComparisonResult.OrderedAscending }

String用の場合も、少し変わった形のソートですから、省略した記述は避けたほうが良いでしょう。

 

一応、使用例も紹介します。非常に簡単です。いつものように、iOS実験専用アプリから抜き出しました。

// ソート用メソッドを使ってみる
let iAryKey1: [Int] = modelRecCard25.sortF(fNumber_i)  // Int型の項目でソート
let iAryKey2: [Int] = modelRecCard25.sortF(fRate_f)    // Float型の項目でソート
let iAryKey3: [Int] = modelRecCard25.sortF(fName_s)    // String型の項目でソート

メソッドの引数として、項目番号を指定します。項目番号を間違えないようにと、項目番号に名前をつけた変数(定数)を使うのが基本です。

どの項目のソートでも、たった1行書くだけで、ソート結果が得られます。昇順に並んだレコード番号の配列です。あとは、そのレコード番号を使ってモデルクラスにアクセスするだけです。

 

今回の機能追加により、記録カードのモデルクラスには、各項目でのソート機能が追加できました。少し前に検索機能も追加していますから、基本的な機能として、検索とソートが揃ったことになります。他に、ファイルへの保存や、タブ区切りテキストの生成もあり、最低限の機能は満たせたと思います。

あとは実際のアプリで使ってみて、不足する機能を追加するだけです。十分に使い物になると判断していますから、積極的に使いたいと思います。過去に作ったアプリでも、機能追加の際に、このモデルクラスと置き換えてみようかと考えています。

ライブラリ化したモデルクラスは2つに増えましたから、3つ目のモデルクラスとなるモデル候補を物色中です。もし変わったモデルクラスが出来上がったら、また紹介したいと思います。

 

(使用開発ツール:Xcode 6.3.2, SDK iOS 8.3)

2015年7月1日水曜日

共通モデルのライブラリ化を試作(内部変更編)

共通で使えるモデルクラスをライブラリ化する第1段として、記録カードのモデルクラスを作る話を紹介しました。今回は、その後の状況を報告します。

 

記録カードのモデルクラスが一段落したので、2つ目のモデルクラスを作りました。記録カードと少し似ている、キー付きカードのモデルクラスです。記録カードにキーが追加されたと考えて、ほぼ間違いありません。記録カードはArreyに相当し、キー付きカードはDictionaryに相当すると考えれば、中身を想像しやすいと思います。

記録カードを作った経験があるので、テストコードも含めて、おおよそ1/3の時間で作り終わりました。クラスに限らず何かのソフトを作る場合には、コーディングしている時間よりも、仕様を考えている時間のほうが多いです。

2つ目のキー付きカードは、記録カードという前例があります。そのため、どんな形で内部データを保存するのか、どんなメソッドがあればよいのか、どんな検査機能を付けなければならないのか、などで悩みませんでした。また、テストコードも似た形で作れるので、その点でも悩みませんでした。まさに、スイスイと作れたという感じです。

肝心なのは、内部データの持ち方ですね。最初からAnyかAnyObjectと決めていたので、どちらにするか決めるだけです。4つの基本データ型だけでなく、将来はクラスのインスタンスも持てるようにしたいはずです。ほとんどのクラスがNSObjectのサブクラスですから、それと相性の良いAnyObjectが適しているでしょう。というわけでAnyObject配列を選びました。

キー付きというモデルなのも関係しているのか、記録カードのモデルクラスよりも、少しスッキリした内部構造に仕上がりました。AnyObject配列を選んで正解だったようです。

 

2つ目のモデルクラスの結果が良かったので、記録カードのモデルクラスも、内部データを持つ配列をAnyObject型に変更してみました。

今までは、データ型ごとに配列を用意したので、配列が4つもあります。それを、1つのAnyObject配列に置き換えるだけです。余計なインデックス変換も不要になり、コードが少し短くなります。

変更前のモデルクラスは別名で保存しておき、変更作業を進めました。実際に作業してみると、30分ぐらいで終了です。内部の仕様だけが変更なので、テストコードはそのまま使えます。コンパイルエラーを全部消して実行すると、一発で通りました。変更完了です。

よく考えると、変更する箇所は多いものの、変更の内容自体は単純でした。4つの配列を消して、1つのAnyObject配列を追加します。インデックスを変換している箇所では、変換している1行を削除し、新しいインデックスに置き換えるだけでした。また、AnyObject配列からデータを読む箇所では、それぞれのデータ型へキャストする指定を追加します。

このようにパターン化できる作業しかないため、かなり慎重に進めても、30分ぐらいの時間で終わりました。当然ですが、一番最初に作ったのは、新しいAnyObject配列の追加と、インデックスの使い方を説明するコメントです。このコメントを見ながら作業するので、間違いを起こさず更新できたのでしょう。

 

そのコメント行を紹介します。まずは、変更前のコメントから。

// ======================================== 3つのインデックスがある
// RecIdx:レコードの並び順を示すインデックス
// FldIdx:項目(フィールド)の並び順を示すインデックス:この値からDataIdxを得て、それでデータへアクセスする
// DataIdx:データ配列内で、どの行(位置)に入っているかを示すインデックス:FldIdxをキーにして、cAryDataIdxから得る
// ============================== 次のように利用する
// cAryName[FldIdx] -> String (項目名)
// cAryType[FldIdx] -> ZmType (データ型)
// cAryDataIdx[FldIdx] -> DataIdx
// cAryDataXXX[DataIdx][RecIdx] -> Data (項目値:4種類のデータ型)
// ========================================

インデックスを変換するDataIdxが使われています。続いて、変更後のコメントです。

// ======================================== 2つのインデックスがある
// RecIdx:レコードの並び順を示すインデックス
// FldIdx:項目(フィールド)の並び順を示すインデックス
// ============================== 次のように利用する
// cAryName[FldIdx] -> String (項目名)
// cAryType[FldIdx] -> ZmType (データ型)
// cAryDataAny[FldIdx][RecIdx] -> AnyObject (項目値:中身は4種類のデータ型)
// ========================================

3つのインデックスから、2つのインデックスへと変更されたのが、もっとも大きな点でしょう。DataIdxがなくなり、FldIdxで直接指定できる形になりました。また、項目値のデータ型が、変数上はAnyObjectだけに変わっています。

 

変更したメソッドのうち、主なものも紹介します。まずは、getメソッドから。

// 値の取得(変更前)
func getDataIntF(rRecIdx:Int, _ rFldIdx:Int) -> Int {
    if cAryType[rFldIdx] != .ZmInt { zSendErrMsgF("ERROR:ZMR_GDI:項目定義のデータ型がIntでない"); return -999 }
    if rRecIdx < 0 || rRecIdx >= cRecCount { zSendErrMsgF("ERROR:ZMR_GDI:レコード位置が範囲外"); return -999 }
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    let iInt: Int = cAryDataInt[iDataIdx][rRecIdx]
    return iInt
}
// 値の取得(変更後)
func getDataIntF(rRecIdx:Int, _ rFldIdx:Int) -> Int {
    if cAryType[rFldIdx] != .ZmInt { zSendErrMsgF("ERROR:ZMR_GDI:項目定義のデータ型がIntでない"); return -999 }
    if rRecIdx < 0 || rRecIdx >= cRecCount { zSendErrMsgF("ERROR:ZMR_GDI:レコード位置が範囲外"); return -999 }
    let iInt: Int = cAryDataAny[rFldIdx][rRecIdx] as! Int
    return iInt
}

変更後の処理では、インデックスを変換している行が消えて、メソッドが1行分だけ短くなりました。ソースコード全体を眺めても、たいして短くならなかったというのが感想です。内部構造としては、余計なインデックス変換がなくなった点が一番大きいでしょう。

AnyObject型の配列cAryDataAnyから、データを取り出す際にはキャストが必要です。でも、キャストのために数文字を追加するだけですから、あまり変わっていない印象ですね。

getメソッドと違って、setメソッドは変更ありません。Intなどのデータ型の値を、AnyObject配列へ入れるだけですから、特別な指定は不要です。

 

続いて、データを保存する配列の初期化です。

// データ用の配列の初期化(変更前)
// 項目ごとに配列を生成して、その位置(インデックス番号)を記憶する
private func setupArrayF() {
    for i in 0..<cFldCount {  // i=FldIdx
        switch cAryType[i] {
        case .ZmInt :
            cAryDataIdx.append(cAryDataInt.count)  // count値がインデックス値より1つ大きいので、作成前に保存する
            cAryDataInt.append([Int]())
        case .ZmFloat :
            cAryDataIdx.append(cAryDataFloat.count)
            cAryDataFloat.append([Float]())
        case .ZmBool :
            cAryDataIdx.append(cAryDataBool.count)
            cAryDataBool.append([Bool]())
        case .ZmStr :
            cAryDataIdx.append(cAryDataStr.count)
            cAryDataStr.append([String]())
        }
    }
}
// データ用の配列の初期化(変更後)
// データ保存用の配列を初期化して、項目ごとに配列を生成する
private func setupArrayF() {
    cAryDataAny = [[AnyObject]]()      // データ保存用の配列を初期化
    for i in 0..<cFldCount {  // i=FldIdx
        cAryDataAny.append([AnyObject]())    // 項目ごとに、空の配列を追加
    }
}

保存場所を指すインデックス値DataIdxを保存する必要がなくなったのと、配列が1つになったので、かなり短くなりました。ちなみに、これだけ短くなったのは、ここの1箇所だけです。

 

このような感じで、無事に内部データをAnyObject配列に更新できました。

ただ更新しただけでは、とくにメリットはありません。しかし、内部配列がAnyObject型になったことで、いろいろなデータ型を簡単に入れる準備ができました。4つのデータ型とは違うデータ型へも、苦労せずに対応できそうです。

もちろん、以前の内部配列の構造でも、新しいデータ型へは対応できます。AnyObject配列を追加して、そこへ入れれば良いだけです。でも、データ保存用の配列が少しずつ増えていくのは、あまり良い形とは言えません。スッキリした構造から、どんどんと離れていきますから。

今回の変更は、将来の機能拡張の準備として、大きな意味があったと思います。今後も、このモデルクラスを改良して、少しずつ良くしていく予定です。

 

(使用開発ツール:Xcode 6.3.2, SDK iOS 8.3)