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)

0 件のコメント:

コメントを投稿