Objective-C/Xcode Tips

何度、検索かけても、忘れているようなこと。メモ。

NSSet -> NSArray 変換

NSSet *nss = [[NSSet alloc] initWithObjects:@"AA", @"BB", @"CC", @"DD", nil];
NSArray *nsa = [nss allObjects];

任意のインスタンス変数のクラス名を調べる

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

#pragma mark の謎

簡単に言えば、ソースコードを Xcode で扱った際に見出しをつけられる。

 

 

NSOutlineView と item

多層構造のデータ表示の仕方がわからない、と言っていたが、試行錯誤の末、できた。

Person – Child – GChild という階層を持つデータがあったとき、適切に実装すると以下のように表示が可能だ。

ポイントは、 item をうまく使うこと。

具体的には

-(NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
{
    if(item){
        if([item isKindOfClass:[Child class]] ){
            Child *c = item;
            return [c.gchildren count];
        }
        if([item isKindOfClass:[Person class]] ){
            NSLog(@"this item is Person");
            return [[item children] count];
        }
        return 0;
    }else{
        return [self.objects count];
    }
}

などとする。

直感的には item = 行(インスタンス1つ)という理解でいいと思う。
トップ階層(item が定義できない)は、self.objects count などとしているわけですね。

参考

・ここでの質疑応答

item という概念がわからないというある人の問題提起に対して林さんが

私の場合ですが、実データを入れるクラスとOutlineのアイテムを別々のクラスにして、アイテムの方のクラスのプロパティに実データのクラスのインスタンスを入れるように実装したことがあります。アウトラインに表示する時にものによってそれに対応するデータのクラスが異なっていたので、アウトラインビューのデータソースのアイテムは表示のためのものと割り切りました。

と応えている。

これは考えてみれば当たり前の話で、NSOutlineView からしてみれば、どんなデータが要素になるかわからない。
だから、そこら辺はコーダーの方で処理してくれよ、という話になる。

・YouTube の動画

動画は流れを掴むのにちょうどいい。

31:53 TableColumn の identifier の設定。
36:30 NSOutluneView と管理クラスの結び付け(datasource 経由

あたりは「百聞は一見にしかず」でしょう。

・OsiriX/Horos/HorliX における NSOutlineView

調査中。

・NSTreeView を使う

この記事で NSTreeView を使っていた。
手が空いたら試す。

・公式解説

 

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」あたりで検索かけましょう。ただ、これ、オブジェクト間通信を使えばいいような? 設計的に美しくないし、可読性落ちるけどさ→やはり通知でできましたとさ)

 

 

Objective-C のわかりにくいところ

まだ、完全に理解していないところ。

・@property と @synthesize

なお、@synchronized は排他制御に関する事柄なので、全然別の概念。

(→まずまず理解)

・NSNotificationCenter 関係

これは、ワイが、というより、初学者すべて。
難しく説明しすぎのような? オブジェクト間で通知したい時に使う。
あるクラスが通知を受け取りたい時、その仕組みが必要なので、イニシャライザに NSNotificationCenter も組み込んでおく。
のような感じでいいのでは?

 

 

CoreData 再び

以前に『Objective-C 再入門 -1.0.2- coreData を使う』という記事を書いた。

そこでは、余計なことを気にしないようにコマンドラインツールで CoreData を使ったんだが、実務的にはこんな使い方はしない。大抵の場合、iOS プロジェクトで GUI 環境で使う場合が多いと思う。

そういうわけで、今回は GUI 環境で使うときの覚え書き。

諸々の事情で MacOS だけどさ。

最も簡単な Core Data サンプル

デフォルトに 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 の名前が、そのままメンバー変数の名前になる(こういう仕様は使ってみないとわからない)ので、モデル上での作業がかなり重要ということがわかります。

.xcdatamodeled でのクラス定義

話を戻して。

今回は、クラスを二つ用意して、その間に 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 ファイルの場所

ところで、永続化したとき、この情報を永続化している sqlite ファイルはデフォルトではどこにあるのだろうか?

実は、最初どこにあるのか不明で本当に永続化できているか確認のしようがなかった。

あたりをつけて探したら見つかったんだが、わかりにくいぞ(笑)。

アプリ自体の名前が CoreDataTest として、
bundle indentifier が com.sample.CoreDataTest2
モデルの名前が CoreDataTestDB
ユーザーの名前が hoge さんだったとすると

/Users/hoge/Library/Containers/CoreDataTest2/Data/Library/Application Support/CoreDataTest/CoreDataTestDB.sqllite

になる。長っ。

ココアアプリの場合、デフォで作成される persistentContainer をそのまま流用すれば、このファイルは自動で作ってくれる。説明が前後するが、前回のコマンドラインツールの場合は、(古い書き方のせいで)自分で作る必要があった。

fetchRequest

永続化はできたので、今度はこれら情報を取り出したい。

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 に変換しておくと後の処理が楽でしょうか。

NSTableView を組み込む

Xcode 上での TableView の操作に関しては、(Swift & iOS になってしまうが)

CoreData と TableView の使い方

が参考になるでしょうか。

Tips

本題とは直接関係ないが、気になったことなど。

レガシーコード

レガシーなプロジェクトでは NSPersistentContainer を使わないようだ。

こちらの 「iOS10 より前の・・・」項など参照。

sqlite ファイルを削除するときは思い切りよく

いまいち仕組みがわかってないのだが、XXX.sqlite を動作させると XXX.sqlite-shm と XXX.sqlite-wal というファイルも生成される。

通常使用なら気にしなくていいのだが、元の XXX.sqlite を削除した場合、ここで使っていた情報は生成された二つのファイルにも書き込まれているので、これらも削除しないとアプリが新規 sqlite ファイルをオープンできずエラーが出る。

テストの時にうっかりやってしまうと「え? ワイ何か重大なミス犯したかな」と一瞬固まるので(笑)、こういう場合は思い切りよく削除しましょう。
テスト環境だし。

予定

CRUD 操作・・あたり。

(続く)