いや、引退するわけじゃないんですが、案件に追われて育成している時間が取れない。
なお、アリエス杯は、ワイが A 決2位、他主要メンバーは B決3位と B決2位。
割と順当な成績。

ウマ娘関連のネタが多いでしょうか
いや、引退するわけじゃないんですが、案件に追われて育成している時間が取れない。
なお、アリエス杯は、ワイが A 決2位、他主要メンバーは B決3位と B決2位。
割と順当な成績。
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」あたりで検索かけましょう。ただ、これ、オブジェクト間通信を使えばいいような? 設計的に美しくないし、可読性落ちるけどさ→やはり通知でできましたとさ)
このところ、Objective-C ガラミの記事を書いていたせいか、他の言語のこと忘れちょる。
ネットに落ちている簡単なレクチャー読んでリハビリ。
こういう時に江添本とかはよくない。
『一週間で身につくC++ 言語の基本』シリーズ
こういうの、いいねー。
本当にメモ。
ostream 基本。
まだ、完全に理解していないところ。
なお、@synchronized は排他制御に関する事柄なので、全然別の概念。
(→まずまず理解)
・NSNotificationCenter 関係
これは、ワイが、というより、初学者すべて。
難しく説明しすぎのような? オブジェクト間で通知したい時に使う。
あるクラスが通知を受け取りたい時、その仕組みが必要なので、イニシャライザに NSNotificationCenter も組み込んでおく。
のような感じでいいのでは?
以前に『Objective-C 再入門 -1.0.2- coreData を使う』という記事を書いた。
そこでは、余計なことを気にしないようにコマンドラインツールで CoreData を使ったんだが、実務的にはこんな使い方はしない。大抵の場合、iOS プロジェクトで GUI 環境で使う場合が多いと思う。
そういうわけで、今回は GUI 環境で使うときの覚え書き。
諸々の事情で MacOS だけどさ。
デフォルトに Person クラス(プロパティは name )を追加して AppDelegate の applicationDidFinishLaunching メソッドに以下のコードを実装。
_persistentContainer = [self persistentContainer];
NSManagedObjectContext *context = _persistentContainer.viewContext;
Person *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person"
inManagedObjectContext:context];
person.name = @"taro";
NSError *error = nil;
[context save:&error];
これで、永続化されている。
見出しの通り。
モデルで Person というクラスを想定した場合、自動生成した時、Person+XXX というファイルが4つほどできるが、元の Person というクラスをあらかじめコーディングしておくのは不要。
というのは、現在の Xcode (Ver 14)とは動作が違うのか @property などでせこせこ Person クラスを作成してから、自動生成させる、みたいに書いてある記事があって、???となっていたから。
確かに Java の hibernate では、クラスのコーディングはコーダーの仕事だが、CoreData では xxx.xcdatamodeled 上でクラスを定義するので、そこから考えても不要。
なんか、おかしな記事だった。
今回は、One To と Many To も定義したが、自動生成すると、例えば、One To Many の One 側のクラスは

となる。
Many の側として Child クラスを想定したが、Person からしたら Child のインスタンスは複数取りえるのでアクセサの返値の型は
NSSet<Child *>
になるわけですね。
その記事、その説明もなんかおかしかったな。
CoreData に限らず、ORM の使い方をなにか誤解している人は多いので、リアルでもこういう人への対処は注意ですね。
あと、relationship の名前が、そのままメンバー変数の名前になる(こういう仕様は使ってみないとわからない)ので、モデル上での作業がかなり重要ということがわかります。
話を戻して。
今回は、クラスを二つ用意して、その間に One To と Many To の関係性(relationship)を入れます。
わかってしまえば、作業は少ない。
One 側のクラス Person の定義は↓。

Many 側のクラス Child の定義は↓。

これらを定義したら、いつものように
menu -> Editor -> Create NSManagedObject subclass…
で、クラスを自動生成させます。
機能は何もありませんが、ビルドするだけなら、この時点でランさせてもエラーは出ないはずです。
問題となるのは、実際に永続化するときでしょう。
また、循環参照の問題とか起こる・・・のか?
もうちょっと上手い書き方もあるような気もするのだが、永続化自体はできた。
context を用意して
Child *child = [NSEntityDescription insertNewObjectForEntityForName:@"Child"
inManagedObjectContext:context];
child.name = @"taro";
Person *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person"
inManagedObjectContext:context];
person.name = @"akiba";
[person addChildrenObject:child];
として、CoreData が生成してくれたメソッド addChildrenObject で person に child を追加する。
save とすると、


と sqlite に永続化できた。
この時、ZCHILD_ZPERSON_INDEX に関係性の情報が格納されている、ということなんだろう。
しかし、循環参照は起きなかったが、どこで context を生成するとか、もうちょっと経験を積む必要がありそう。
ところで、永続化したとき、この情報を永続化している sqlite ファイルはデフォルトではどこにあるのだろうか?
実は、最初どこにあるのか不明で本当に永続化できているか確認のしようがなかった。
あたりをつけて探したら見つかったんだが、わかりにくいぞ(笑)。
アプリ自体の名前が CoreDataTest として、
bundle indentifier が com.sample.CoreDataTest2
モデルの名前が CoreDataTestDB
ユーザーの名前が hoge さんだったとすると
/Users/hoge/Library/Containers/CoreDataTest2/Data/Library/Application Support/CoreDataTest/CoreDataTestDB.sqllite
になる。長っ。
ココアアプリの場合、デフォで作成される persistentContainer をそのまま流用すれば、このファイルは自動で作ってくれる。説明が前後するが、前回のコマンドラインツールの場合は、(古い書き方のせいで)自分で作る必要があった。
永続化はできたので、今度はこれら情報を取り出したい。
persistentContainer を定義した同一クラスで情報を取り出す適当なメソッドを作成し以下のように実装した。
NSManagedObjectContext *context = self.persistentContainer.viewContext;//or newBackgroundContext
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:NSStringFromClass([Child class])];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
NSArray *results = [context executeFetchRequest:fetchRequest error:nil];
NSUInteger i = 0;
for (Child *data in results) {
//do something.
NSLog(@"data: %lu name: %@", (unsigned long)i, data.name);
i++;
}
このメソッドを呼び出す(一番簡単には menu item のアクション実行先をこのメソッドにする)。
Target Output には、Child クラスの name が永続化された個数分、表示される。

できてますね。
Child クラスは Person クラスと One To Many の関係があるので
NSLog(@"data: %lu name: %@ Person name: %@", (unsigned long)i, data.name, data.person.name)
とすると Person の name も表示されます。
なんか ORM っぽくなってきました。
Person を読んでから、関係性の情報を使って Child の name を取得・表示する。
for (Person *data in results) {
//do something.
NSLog(@"data: %lu Person name: %@", (unsigned long)i, data.name);
NSArray *children = [data.children allObjects];
for(Child *child in children){
NSLog(@" Child name: %@", child.name);
}
i++;
}
NSSet で返されるので、一括して NSArray に変換しておくと後の処理が楽でしょうか。
Xcode 上での TableView の操作に関しては、(Swift & iOS になってしまうが)
が参考になるでしょうか。
本題とは直接関係ないが、気になったことなど。
レガシーなプロジェクトでは NSPersistentContainer を使わないようだ。
こちらの 「iOS10 より前の・・・」項など参照。
いまいち仕組みがわかってないのだが、XXX.sqlite を動作させると XXX.sqlite-shm と XXX.sqlite-wal というファイルも生成される。

通常使用なら気にしなくていいのだが、元の XXX.sqlite を削除した場合、ここで使っていた情報は生成された二つのファイルにも書き込まれているので、これらも削除しないとアプリが新規 sqlite ファイルをオープンできずエラーが出る。
テストの時にうっかりやってしまうと「え? ワイ何か重大なミス犯したかな」と一瞬固まるので(笑)、こういう場合は思い切りよく削除しましょう。
テスト環境だし。
CRUD 操作・・あたり。
(続く)