2023 年 4月
黄砂が舞い、気温が乱高下した 2023 年 4 月、皆様、いかがお過ごしでしょうか?
仕事がひと段落ついたので、日記調雑談。
cocoa
仕事がひと段落といっても、即、お金になるようなことをやっていたわけではない。
今回は、マックのココアアプリを作成する上での必須技能みたいなものを身につけようとしていた。
今までにも Objective-C の話題は出していたじゃないか?と言われそうだが、それはちょっと違う。
言語の仕様を覚えるのとアプリ作成に必要な知識を習得するのは、別の話だ。
配列や変数の使い方は何をするにしても必要な基本スキルだが、アプリを作成するとなるとウインドウやその他 UI パーツの使い方などを身につけていく必要がある。
最近では、ORM やグラフィックも各 OS 毎に独自化が進んでいるので、これもある程度使えるレベルにしておく必要がある。
そういう意味での「ひと仕事」だ。
小旅行
ひと仕事終わった後の達成感はいいものだが、気分転換もしたくなる。
ワイは、日帰り〜一泊レベルの小旅行に出たりする。
どこに行こうか?
(続く)
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 を使った際に右インスペクタに表示される参照情報。
気になったのでまとめた。
NSOutlineViewDataSource
Navigating Hierarchical Data Using Outline and Split Views
サンプル(↓)もあるのだが、立派すぎて初学者には参考にならないかな。

akiba
さようなら、全てのウマ娘
いや、引退するわけじゃないんですが、案件に追われて育成している時間が取れない。
なお、アリエス杯は、ワイが A 決2位、他主要メンバーは B決3位と B決2位。
割と順当な成績。
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」あたりで検索かけましょう。ただ、これ、オブジェクト間通信を使えばいいような? 設計的に美しくないし、可読性落ちるけどさ→やはり通知でできましたとさ)
