CoreData と NSOutlineView

CoreData と NSTableView に関して、いくつか書いたが、これらは練習。

NSTableView では entity 同士が relationship で繋がれたような階層的なデータは扱えないので、NSOutlineView というものが用意されている。

NSTableView の時と同様、Cell Based と View Based があり、挙動が違うようなんだが、イマイチ理解してない。
どちらが作成しやすいかといえば Cell Based 。

なお、デフォは View Based になっているようなので、NSOutlineView を貼り付けたら、右インスペクタを操作して Cell Based に設定しておく。

NSOutlineView

いろいろ調べたんだが、芯食った解説(特に Objective-C & MacOS 環境)に巡り合わなかったので、いきなり CoreDtata との連携を考えるのではなく NSOutlineView 単独で適当なデータを表示させる方向でいく。ここは慎重に。

で、作成。

クラス Person と NSMutableArray<Person *> *people を用意して *people を表示させたい。

階層がある場合には、セルクリックで展開できる。

まずは、ポイントを解説。

階層があるので、セルを表示するのに row と column では役不足。
そこで item という概念が登場する。
実態としては、この場合は *people を反映したなにかなんだが、多分、最初はなんのことかわからないと思う。

先に進もう。

NSOutlineDatasource プロトコルを適用した NSOutlineView を管理するクラスを作成する。
プロトコルを満たすために最低4つのメソッドを実装する必要がある。
なお、データを表示するだけなら、NSOutlineViewDatasource プロトコルを満たすだけでいい。delegate までは必要ない

4つのメソッドは

-(NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item;

-(BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item;

-(id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item;

-(id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item;

で、例えば、最初の outlineView: numberOfChildrenOfItem: は

-(NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item{
    //return !item ? [self.people count] :[[item children] count];
    if(item){
        return [[item children] count];
    }else{
        return [self.people count];
    }
}

となる。

DataSource と item

まだ、公式ドキュメントで確認などはしていないのだが、動作から考えるに

・DataSource としてリンクさせたクラスのコレクションがテーブルで表示させる範囲

・コレクションに含まれるクラスが item

なのかなと推測しているし、例外は経験したことがない。

上の場合で言えば、NSMutableArray<Person *> *people 自体は要素ではないから、item ではない。

推測する、とぼかしたのは、コレクションすべてを試したわけではないから。
今のところ、取り扱いが簡単な NSMutableArray で試してうまく表示できたが、他の、例えば NSDictionary などは一切試していない。というか試したが、見事にコケた。書き方が悪いのか、それとも、仕様上そうなってないのかは不明。
チャレンジされた方がいたら、教えて欲しい。

また、「コレクションに含まれるクラスが item」とは書いたが、他のクラスを Person の子要素に含んでいても問題なく動く。

異なるクラスを 親-子-孫 の関係にした場合

「他のクラスを Person の子要素に含んでいても問題なく動く」とは書いたが、Person の子要素として Child クラスを設定し、さらにその子要素として GChild を設定した場合は、ダメっぽい。

        NSString *className = NSStringFromClass([item class]);
        NSLog(@"ClassName is: %@", className);

を適当な位置に入れたが、返されるのは Person のみ。
しかし、NSStringFromClass([item class]) という書き方は便利ですね。item のところにクラスがよくわからないインスタンスに置き換えれば、そのクラス名を返してくれます。

おそらく DataSorce =  NSMutableArray<Person *> *people としたため、item のクラスとしては Person 以外取りようがないからだと思う。

しかし、DataSorce =  NSMutableArray<NSObject *> *object と一般的にすると item は任意のクラスを取りうる。しかし、このやり方で3階層以上を実装するのは頭こんがらがりそう。

まあ、大雑把に表示はできたんですが。→自己解決。別記事にします。

NSOutlineView と CoreData を結びつける

結びつけること自体は、それほど難しくないと思う。

ただ、 One To Many の関係にある relationship の Many の側はことごとく NSSet になってしまうので、いまいち使いづらい。
いい方法ないだろうか?(→ NSSet 公式ページ

クラスを生成した「後に」属性値を追加する

普通にできるっぽい。
.xcdatamodeled に attr を追加。該当するクラスに @property …. を追加する。

これができないと、ちょっとした変更をするたびに、クラスを生成する必要があるので、当然か。

実用的には、クラスと relationship をとりあえず決めておいて、細部は後で編集する、という使い方でいいかもしれない。

 

(続く)


気が早いかもしれないが、CoreData でデータも作成しておく。

いつものように Person – Child という親子関係の Entity を作成し、永続化させると sqlite には以下のように保存される。


対応するコードは以下の通り。


    Child *child = [NSEntityDescription insertNewObjectForEntityForName:@"Child"
                                                    inManagedObjectContext:context];
    child.name = @"taro";
    
    Child *child2 = [NSEntityDescription insertNewObjectForEntityForName:@"Child"
                                                   inManagedObjectContext:context];
    child2.name = @"kana";
     
    Person *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person"
                                                    inManagedObjectContext:context];
     
    person.name = @"akiba";
     
 
    NSSet *childs = [NSSet setWithObjects:child, child2, nil];
     //[person addChildrenObject:child];
    [person addChildren:childs];

Child のインスタンスを一度に複数登録する場合には、NSSet を使うので、そのように変更した。

 


アップル公式の参考情報

Xcode で NSOutlineView を使った際に右インスペクタに表示される参照情報。
気になったのでまとめた。

Delegate and Data Sources

NSOutlineViewDataSource

Navigating Hierarchical Data Using Outline and Split Views
サンプル(↓)もあるのだが、立派すぎて初学者には参考にならないかな。

akiba

CoreData と NSTableView

CoreData に関する記事はいくつか書いてきたんだが、実用的には NSTableView とリンクして使いたい。

なのだが、cocoa & Objective-C 環境で作成されたサンプルがないんだな、これが(笑)。

UITableView になってしまうが iOS & Swift なら結構あるんですけどね。
まあ、ないと iPhone アプリでデータのテーブル表示できないでしょうから、当然でしょうけど。

NSTableView 単独になってしまうけど、かろうじて見つけたのが、これ

おそるおそる現環境(MacOS Ventura, Xcode 14)でビルド。

おお、修正なしで一発動作。よしよし。

解説記事も充実している(態度豹変)。

 

このサンプルをベースに CoreData のデータと NSTableView のセルを結びつける。

結果的には、まあまあできた。

ポイントは、NSTableDataSource プロトコルと NSTableViewDelegate プロトコルに必須の二つのメソッドを実装するところでしょうか。

こういうとき「動く」サンプルは便利だと思うのだが、サンプルを少々訂正するだけでいいし、だから、その意味を掴みやすい。

ワイはこんな風に実装した。


// NSTableViewDataSource Protocol Method

- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView {
    //return self.numbers.count;
    return self.personname.count;
}

// NSTableViewDelegate Protocol Method

-(NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
    NSString *identifier = tableColumn.identifier;
    NSTableCellView *cell = [tableView makeViewWithIdentifier:identifier owner:self];
    
    if ([identifier isEqualToString:@"personname"]) {
        //cell.textField.stringValue = [self.numbers objectAtIndex:row];
        cell.textField.stringValue = [self.personname objectAtIndex:row];
    } else {
        cell.textField.stringValue = [self.personage objectAtIndex:row];
    }
    
    return cell;
}

元コードではセル値の供給元になっている numbers は NSArray として定義されていたが、これだと可変値は扱いにくい。データを追加・削除しやすいように NSMutableArray に変更した。

これはなかなか勉強になった。

最終的には、sqlite のデータが書き変わったら、テーブルのセル値も追従して変えてくれるようにしたいんだが、現状だとちょっと難しいかな。(よくわからない人は「NSTableView update」あたりで検索かけましょう。ただ、これ、オブジェクト間通信を使えばいいような? 設計的に美しくないし、可読性落ちるけどさ→やはり通知でできましたとさ)