NSArrayController利用時のNSTableViewへのinsertをカスタマイズ

注意事項
NSArrayControllerを利用したNSTableViewで、insert位置を変更する際につまずいたのでメモ。

結論:NSArrayControllerを利用したNSTableViewで独自にinsertやaddを実装する時は、NSArrayControllerでの選択行と、NSTableViewでの選択行とが別であることに注意する必要がある。

■選択行の下の行にinsertしたい

NSTableViewを扱う際、NSArrayControllerを使うと細かい処理を書かなくてよくなり便利。ただ、逆にそこから細かい処理をしようとするとかえってややこしくなった。

まず、以下のようなテーブルがある。これはNSArrayControllerにbindしている。また、NSArrayControllerはAppControllerにあるNSMutableArrayにbindしている。

f:id:nashikachi:20160319234844p:plain:w300

上記の画像のように、「testA」を選択している時に「+」ボタンを押すと、

f:id:nashikachi:20160319235115p:plain:w300

こんな感じに、「testA」の下に新しい行が挿入されてほしい。

しかし、この「+」ボタンをArrayControllerの「add:」に接続していると、最後の行に加えられる。
f:id:nashikachi:20160319235117p:plain:w450

つまり以下のようになる。
f:id:nashikachi:20160319235124p:plain:w300

そこで、「+」ボタンの接続先をArrayControllerの「add:」ではなく「insert:」に変える。すると以下のようになる。
f:id:nashikachi:20160319235126p:plain:w300

惜しい。選択行の上ではなく、下に挿入されてほしかった。

Interface Builderからの設定では期待する動作を実現できないようなので、AppControllerにメソッドを実装する。

■コードでのArrayControllerへのadd、insert

注意すべきは、表示上の(TableView上の)選択行と、ArrayController上の選択行が異なる点である。Interface BuilderでArrayControllerに直接add:やinsert:に接続した場合の動作では、TableView上の選択行が考慮される。しかし、コード上でArrayControllerにadd:やinsert:をする場合、tableView上の選択行は考慮されず、ArrayController上の選択行は常に0行目になる。これに気付かずしばらくはまった。
TableView上の選択行をArrayControlerに伝える処理が必要となる。


AppController.m

- (IBAction) add:(id)sender
{
    NSLog(@"選択行: tableView:%lu ArrCon:%lu", [_tableView selectedRow], [_arrCon selectionIndex]);

    if ([_testArray count] > 0) {
        NSUInteger selectedRow = [_tableView selectedRow]; //TableViewでの選択行を取得
        if (selectedRow == -1) { //選択されていなければ
            selectedRow = [_testArray count];//末尾を選択行に設定
        }
        [_arrCon insertObject:[[Contents alloc] init] atArrangedObjectIndex:selectedRow];
        [_arrCon setSelectionIndex:selectedRow]; //arrayControllerの選択行を変更
 
        [_tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:selectedRow] byExtendingSelection:NO]; //tableViewの選択行を変更
    } else {
        [_arrCon insertObject:[[Contents alloc] init] atArrangedObjectIndex:0];
        [_tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:NO]; //tableViewの選択行を変更
    }
}

_tableViewはTableViewのアウトレットで、_arrConはArrayControllerのアウトレット、_testArrayは表示するデータの配列であるNSMutableArrayである。Contentsはテーブルに表示するデータをもつクラス。

ポイントは以下の4点
・配列が0かそうでないかで、insert処理を分ける
・ArrayControllerに対し、選択行を設定する
・TableViewに対し、選択行を設定する
・TableViewに対し、reloadDataはしなくてもいい

ArrayControllerに対するinsertはNSMutableArrayと同様に、配列が0の時に0以外のindexを指定するとエラーになる。配列が1以上の時は、最終行+1のインデックスが認められる。そのため、配列が0かそうでないかでinsertのコードを別にしている。

そして、[_arrCon setSelectionIndex:selectedRow]; で ArrayControllerの選択行を、[_tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:selectedRow] byExtendingSelection:NO]; で tableViewの選択行を指定している。tableViewの選択行設定はしなくてもいいが、設定しないとinsertした際に選択状態が外れ、毎回クリックで行を指定する手間が出てくる。(ちなみにこの手間は、Interface BuilderでArrayControllerに直接addやinsertに接続した際は、自動で行われている)

あとは「+」ボタンをAppControllerのadd:メソッドに接続すればOK。なお、コードをもっとスマートに書けるんじゃないかという気はする。

■ソートした場合の懸念点

今回は実装していないので憶測だが、ソートを導入した際はTableViewとArrayControllerとの行番号が一致しなくなる可能性がある。今はソートしないため、TableViewの0行目とArrayControllerの0行目はイコールである。しかしソートした際、TableViewの0行目はArrayControllerの最終行である可能性がある。もしそうであれば、単純にselectedRowで取ってきた値を使っている上のコードは修正が必要になる。その辺のややこしさから逃れたければ、素直にInterface Builderからのaddなりinsertを使った方が賢明。

※2016/4/8 追記
やはり想定通り、ソートをすると上記の場合tableViewのselectedRowでは、行番号が反転し不一致となった。ソートを実装して今更気付いたが、そもそもソートを有効にする場合は、上記で書いている「どこに挿入するか」は意味がなくなる。
そしてこれも今更だが、TableViewをArrayControllerでbindしている場合、オブジェクトの追加、削除、そして選択行取得などの操作は全てArrayControllerに対して命じるべきだと理解した。つまり、上記で行の取得はtableViewに対してselectedRowメソッドを呼ぶことで取得していたが、これもすべきでない。
ソートを実装したサンプルを作成したので、こちらを見た方が参考になると思う。


■凡ミス余談

ちなみに上記の設定を行った際、テーブルで変なところにbindの設定をしてしまっており、妙な動作をして暫くはまっていた。正直、bindをよく知らないまま使っているために生じた問題で普通の人は陥らないと思うが、一応記載する。

今回のプログラムの場合、TableView配下でバインディングが必要なのは
・Table Column の Value
・Table View Cell(NSTextFieldCell) の Value
の2つだけだが、何故か
・Table View の Table Content
にまでbindしていた。

↓不要なバインディング
f:id:nashikachi:20160320012816p:plain:w450

このbind設定をしてしまったことで、以下のような動作が起こり、悩まされた。
・bind先の配列が0の時にadd:をした直後、selectionIndex:の値がおかしい。
・同様にsetSelectionIndex:をした際、設定が反映されない場合がある。
・insertObject: atArrangedObjectIndex: がたまにエラーを吐いて挿入できない。
・tableに対するselectRowIndexes: で選択行を指定してもたまに反映されない。
 等々の不可解な動作を起こした。

その後、このバインディングに気付き、設定を外すと問題は解決した。

■まとめ

ArrayControllerを使いつつ、addやinsert、removeの動作をカスタマイズしようとすると思ったより面倒なので、そんなにこだわりがなければ素直にInterface BuilderでArrayController直結の方が賢い。