NSTableViewDelegate利用時にArrayControllerへオブジェクトが追加されない事象

注意事項
NSTableViewDelegate で TableView のクリックイベントを取得しようとしたら、ArrayController にオブジェクトが追加されない事象に悩まされたのでメモ。

結論は、awakeFromNibでメンバ変数の初期化をしていたことが原因。

事象

ArrayController を利用して、AppController が持つ配列を NSTableView に表示していた。
そこに NSTableViewDelegate を使ってテーブルのクリックを取得するため、NSTableView から AppContorller へ delegate を接続したところ、ArrayController に add しても配列にオブジェクトが追加されなかった(1つ目だけは表示される)。

addしても1行目しか表示されないテーブルと、add時のエラーコード
f:id:nashikachi:20160221215245p:plain

*** -[__NSArrayM insertObject:atIndex:]: index 1 beyond bounds for empty array と怒られているように、表示上1行目だけはデータがあるように見えるが、実際には配列の中にオブジェクトは追加されていない。
配列へのオブジェクト追加は、テーブル下の+ボタンで ArrayController に add を送って追加している。
なお、NSTableView → AppController の delegate 接続を切ると、ちゃんと配列に追加され、表示も反映される。

AppController.h

#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>

@interface AppController : NSObject <NSTableViewDelegate>

@property (strong) NSMutableArray *testArray;

- (NSIndexSet *)tableView:(NSTableView *)tableView selectionIndexesForProposedSelection:(NSIndexSet *)proposedSelectionIndexes;
@end

AppController.m (問題のあるコード)

#import "AppController.h"
#import "Contents.h"

@implementation AppController

@synthesize testArray = _testArray;

- (void) awakeFromNib
{
    _testArray = [[NSMutableArray alloc] initWithCapacity:15];
}

- (NSIndexSet *)tableView:(NSTableView *)tableView selectionIndexesForProposedSelection:(NSIndexSet *)proposedSelectionIndexes
{
    return proposedSelectionIndexes;
}
@end

事象を追っていくと、+ボタンを押したとき、追加されるオブジェクトは生成されていたが、配列の数に変化はなかった。さらに追っていくと、ArrayContorller に add する度、AppContorller の awakeFromNib が呼ばれていることが分かった。

上記のコードの通り、awakeFromNib で 配列である testArray を初期化している。つまり add する度に testArray が初期化されて常に配列が0になっていたため、おかしなことになっていた。

元々 awakeFromNibメソッドで初期化していたのは、Nibからロードされた時の1度しか呼ばれないという思い込みがあって、かつinitメソッドを書くよりかは(若干)書く量が少なくて楽という理由だった。が、awakeFromNib が複数回呼ばれることがあるのであれば、awakeFromNibで初期化するのは止めるべきだろう。
なぜ NSTableViewDelegate を利用すると awakeFromNib が複数回呼ばれるようになるのかは不明。

なので、正解はこれ。
AppController.m (修正版)

#import "AppController.h"
#import "Contents.h"

@implementation AppController

@synthesize testArray = _testArray;

- (id)init {
    self = [super init];
    if(self) {
        _testArray = [[NSMutableArray alloc] initWithCapacity:15];
    }
    return self;
}

- (NSIndexSet *)tableView:(NSTableView *)tableView selectionIndexesForProposedSelection:(NSIndexSet *)proposedSelectionIndexes
{
    return proposedSelectionIndexes;
}
@end

■まとめ

awakeFromNib で 変数の初期化はしない

ヒレガス本(第3版)を見返すと、awakeFromNib でテキストフィールドの値の初期化をしていた。テキストフィールドの値の初期化なら問題はないが、これを見て「何でもawakeFromNibで初期化したらいいんや!」と勘違いした可能性がある。