2010-12-27

NSTableView でソートしたとき、選択された要素をモデル配列から正しく取り出す方法

NSTableView では、見出し行をクリックするとカラムの内容によって行をソートすることができる。ところが、これはモデル(となっている NSMutableArray)をソートしているわけではないから、ソートしたビュー上の並び順とモデル内の要素の並び順は一致していない。

たとえば、もともと「りんご」「みかん」「ぶどう」の順にモデル配列の中に収められているとして、これをビュー上でソートして「ぶどう」「みかん」「りんご」の順に表示されているとする。ここでビュー上で「りんご」を選択すると、そのインデックスは(0 から数えるので) 2 となる。このインデックスを使い、モデル配列から objectAtIndex: で要素を取り出せば、それは「りんご」ではなく「ぶどう」になってしまう。

つまり、ビューでソートした状態でビュー上の選択(のインデックス)に合わせてモデルの要素を取り出すと、ビューの選択とは異なる要素を取り出してしまうことになるのだ。

MacBloggerGlass の場合で言うと、記事の一覧をタイトルまたは日付けでソートした状態で一覧から記事の選択を行うと、表示される記事の内容がずれてしまう。

こうしたビューとモデルにおける要素の並び順の不一致を避けるには、両者を結びつけている NSArrayController を使えば良い。

具体的には、NSArrayController から selectedObjects で要素オブジェクト(の配列)を取り出せば、ビューでソートしているかどうかに関係なく、適切なオブジェクト(つまりビュー上の選択と同じオブジェクト)が取り出せる。

MacBloggerGlass の場合、以下のようなコードを使う(AppController.m より抜粋)。

    Entry *entry = [[feedController selectedObjects] objectAtIndex:0];

feedController が記事一覧(と記事内容)用のモデル配列のための NSArrayController だ。AppController の IBOutlet として確保し、Interface Builder で NSArrayController のインスタンスと結びつけている。記事一覧では複数選択を許していないから、selectedObjects の返す配列の最初の要素を取り出している。

ちなみに、この NSArrayController を使う解決方法はググって見つけた(→Cocoaの日々: NSArrayController を使った NSTableView で選択行の情報を取得する)

NStableView と NSArrayController の関係

では、なぜ NSArrayController からは、ソートの状態に関係なく、適切なモデルオブジェクトを取り出すことができるのだろうか?

オブジェクトの配列をモデルとして、その内容を NSTableView に表示させる場合(配列の要素になっているオブジェクトのプロパティを表のカラムに表示させる、という意味)、Cocoa Bindings では両者を NSArrayController で結びつける。

これは詳細に言えば、NSTableColumn の値として、NSArrayController の arrangedObjects が返す配列の要素のプロパティを結びつける、ということだ。初期状態では、arrangedObjects はモデルの配列の並び順そのままの配列を返す。だから、モデル配列の順序がそのままビューの表示順序になる。一方、NSTableView の見出し行でソートしたときは、NSArrayController の arrangedObjects が返す配列の並び順が変わる。その結果、ビューの表示順序が変わる。

実はビュー(NSTableView)の行がソートされるのはむしろ結果で、実際にはコントローラ(NSArrayController)が返す要素の順序がソートされていたのだ。これは、NSArrayController がソートされた状態のモデル配列を保持している、と言っても良い。だから、ビュー上でソートされた状態のインデックスを使っても、適切なオブジェクトを取り出すことができるのだ。

では、なぜ NSArrayController の arrangedObjects の並び順が変わるのか? それは、NSTableView の見出し行がクリックされたときに NSArrayController に対して setSortDescriptors が呼ばれ、NSSortDescriptor がセットされることによる。これはソートの方法を指示するオブジェクトで、NSArrayController は arrangedObjects を作り出す際に参照するものだ。初期状態ではこれがセットされていないため(ソートされず)、arrangedObjects はモデルの要素順と同じ順序の配列になっている。

  • 見出しをクリックしてからの一連の動きを時系列で並べると以下のようになる。
    1. NSTableView の見出しがクリックされる。
    2. NSTableView が NSArrayController に対して、NSSortDescriptor をセットする。
    3. NSTableView (実際には NSTableColumn) が NSArrayController に対して arrangedObjects を要求する(メソッドを呼び出す)。
    4. NSArrayController は先にセットされた NSSortDescriptor にしたがってソートした配列を返す。

    まとめると、ソートはビューで起きているのではなく、コントローラの内部で起きている、ということになる。だから、コントローラはソートされた状態のインデックスから適切なオブジェクトを取り出すことができる、と。

    iOS アプリではどうする?

    Cocoa Bindings が使えない iOS アプリでは NSArrayController に相当する部分を自前で用意しなければならないはず。なんだか面倒なことになりそうな予感がする。ソートしなければ(できないようにすれば)良いんだけど。そもそも、iOS のアプリで表をソートするアプリって見かけないような……。

    関連リンク

    関連記事

    0 件のコメント:

    コメントを投稿