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)

0 件のコメント:

コメントを投稿