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)