Objective-C 再入門 -1.0.2- CoreData を使う

プログラミング言語の復習などは、文法などから入るのが自然なのだろうが、少々ダルいので、JakartaEE の時と同様、データベース – ORM に手をつける。

Objective-C (Swift でもそうだが)の ORM といえば、coreData なのだが、ここでも GUI は使わずにコマンドラインから扱う。(GUI での使用例はこちら

なお、「なんで、GUI を使わないで、コマンドラインのサンプルで済ませるんですか?」とリアルでも訊かれることがあるが、たぶん、育った環境w

それに加えて swift を勉強していた時に、コマンドラインのサンプルのみからなる教科書を使ったんだが、これがけっこう効果的だったから、という個人的な体験。

サンプルコード

それで、いきなりだが、完成されたコードを示す。

少々、古い書き方だが、動くことは動く。
NSPersistenceContainer を使いたい方は頑張って書き直してください。


#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
#import "Person+CoreDataClass.h"
/*
以下の定義は不要だが、念のため.
というか、あると Person が二つあることになるのでエラー出ます。
@interface Person : NSObject {
    NSString *name;
}
@property(nonatomic)NSString *name;
@end
// Person クラスの実装
@implementation Person : NSObject
@synthesize name;
@end
*/
int main(int argc, const char * argv[]) {
    NSString *modelPath = [[NSBundle mainBundle] pathForResource:@"Model"ofType:@"momd"];
    NSURL *modelURL = [NSURL fileURLWithPath:modelPath];
    NSManagedObjectModel *managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
    
    NSArray* paths = NSSearchPathForDirectoriesInDomains(
                NSDocumentDirectory, NSUserDomainMask, YES);
    NSString* dir = [paths objectAtIndex:0];
    
    NSString* dbPath = [dir stringByAppendingPathComponent:@"sqlite.db"];
    NSURL *url = [NSURL fileURLWithPath:dbPath];
    
    NSError *error = nil;
    NSPersistentStoreCoordinator* coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:managedObjectModel];
    
    if (![coordinator
        addPersistentStoreWithType:NSSQLiteStoreType
        configuration:nil
        URL:url
        options:nil
        error:&error]) {
          NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
          abort();
    }
    
    NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [managedObjectContext setPersistentStoreCoordinator:coordinator];
    
     
    Person *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person"
                                                 inManagedObjectContext:managedObjectContext];
    person.name = @"akiba";
    
    NSError* error2 =nil;
    [managedObjectContext save:&error2];
    return 0;
}

何としてもワンファイルで済ませたかったが、流石に coreData だとそうもいかない。

多分、ネットを彷徨してこの記事に辿り着いた人ならわかると思うが、coreData を使う場合は、「モデルからコードを生成する」という操作がどうしても必要だから。

なお、後述する Xcode 上での操作を完了させ、ビルドすると「書類」フォルダ直下に以下のようなデータベースが作成される。

しっかり永続化されてますね。

3レコードあるのは、3回ランさせたからw

なお、カラムはすべて Z から始まってますが、この接頭子が付く理由は誰もわかってません

このソースコードの解説もしたいところなのだが、まずは動かすための手順を説明していく。

 

Xcode 上での操作

基本的に

モデルの作成→コードの生成

と進む。

モデルの作成

プロジェクト作成時に use coreData にチェックを入れておけば、モデルを記述するファイルは自動でできるのだが、コマンドラインを選んだ場合は、このオプションは選べないので、自分で追加する。

適当なグループでファイルを追加。

上の画面で coreData -> Data Model を選ぶ。

すると選んだグループなどに Model.xcdatamodeled が追加されるので、次はこれを編集。

Entity がテーブル、attribute がカラムになるので、これを意識して .xcmodeled ファイルを編集。

最初はテーブル一つ、カラム一つから始めるといいと思う。

もちろん、複数のテーブルを作成、カラム同士を One To や Many To などで結びつけるという ORM らしい使い方もできるのだが、ここでやってしまうと混乱するだけなのでここではスキップ。

なお、以前の Xcode には、関係性(relationship)をグラフィカルに表示する機能もあったのだが、現在では無くなっているようだ。

コードの生成

モデルが定義できたので、次はモデルを使ってコードを生成する。

menu -> editor -> Create NSManagedObject Subclasses…

を選ぶ。

なお、この時、画面右の生成する言語で swift か obj-c かを指定する。

デフォだと swift が指定されているので、obj-c の時はもちろん Objective-C で。

すると一つの entity に対し4ファイルが生成される。
Person という entity があった場合は

Person+coreDataClass.h
Person+coreDataClass.m
Person+coreDataProperties.h
Person+coreDataProperties.h

の4つ。

+がなんとも特徴的。

このファイルの生成も3通りのやり方がある(Xcode14 では、一つしかないです)のだが、エンティティ一つ程度であれば、深く検討する必要はないでしょう。とりあえず生成して、ビルド時に微調整でいいと思います。

ビルド

操作的には以上で完了です。

単純に save しているだけなので、一回走らせるたびに、sqlite.db に name = “akiba” のレコードが追加されていきます。

 

その他

初学者がまず気にしておく点は、永続化すべきクラスは NSObject を継承しているのではなく、NSManagedObject を継承している点。

だから、インスタンスを生成するときも

Person *person = [[Person alooc] init];

とするのではなく

Person *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person"
                                                 inManagedObjectContext:managedObjectContext];

とします。

また、通常のオブジェクトとは違っているので、データベース操作時には

NSManagedObjectContext

NSPersistentStoreCoordinator

の実体(オブジェクト)が必要という理屈です。

ところで、「coreData objective-c」あたりで検索をかけるとこんな記事がかなり上位にヒットしますが、初学者でこの記事読んで coreData を実際に使えるようになるかというと難しいんではないでしょうか。

記事自体が悪いということではなくて、それだけ情報が少ないってことです。

Xcode14 だとエラーが出ます

Xcode14 上で上記の手順でビルドしてもエラーが出るようです。

Multiple commands produce...

という。

はて?

上の場合は、main.m 以外をコンパイル対象から取り除けば、エラーは消えるんですが、なんか気持ち悪いですね。

発展

今回は、「とりあえず CoreData を使う」ということに主眼を置いたが、使い込むとなると、例えば、検索条件をもっと詳細に設定したいという要求が出てくると思う。

NSPredicate

検索設定に使う。

CoreData を用いてデータ管理を行う』初学者向け

1対多関連のエンティティの検索条件見本

NSPredicate 全構文解説』マニアック

などなど。

Objective-C 再入門 -1.0.1-

前回の記事が評判良かったので、続編。

そのとき提示したサンプルコードは、実は、もうちょっと obj-c っぽく書ける。

以下のようになる。


#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
#import <stdio.h>
// Person クラスの宣言
@interface Person : NSObject
@property(nonatomic)NSString *name;//ここが変わった!
- (void)printName;
@end
// Person クラスの実装
@implementation Person : NSObject
- (void)printName {
    printf("My name is %s \n", [_name UTF8String]);//_ がついていることに注意
}
@end
// 実行プログラム
int main(void) {
    id person;
    person = [[Person alloc] init];
    [person setName:@"akiba"];
    [person printName];
    return 0;
}

一見して気がつくのは、@property なるディレクティブが出現していることだろう。
そのかわり setName というメソッドが消えた。
想像つくかと思うが、フィールド変数のセッター&ゲッター定義は @property で代用がきく。

なお、nonatomic はスレッドセーフでは「ない」ことを明示するオプション。こんなところでスレッドセーフにしても意味がないので、明示した。(指定しないと atomic になる)

また、@property で生成されるフィールド変数 は先頭に _ がつく。

ここまでくると、何とも obj-c っぽい。

逆に、ここら辺を理解せずに obj-c のコードを読むと、「なんだ、これ?」になると思う。

ところで、アクセサ(セッター・ゲッター)によるカプセル化はオブジェクト指向言語の特徴の一つだろうが、obj-c ではここでもまたディレクティブで対応する方法をとっている。

ここまで徹底しているとなんか清々しいですね(笑)。

 

Objective-C 再入門

普通に iPhone アプリを使うなら swift でいいんだろうが、既存の C/C++ ライブラリを組み込んで使うような場合などは、Objective-C も検討せねばならないだろう。

特にレガシーなプロジェクトなぞ、本体が Objective-C で書かれているので、swift に移植するにせよ、ある程度の Objective-C の理解は必須だろう。

なぜ Objective-C はとっつきにくいのか?

ところで、Objective-C のクセの強さは、万人の知るところだが、これは歴史的背景を考えると理解しやすいかもしれない。

C にオブジェクト指向の思想を取り込むために、大雑把に二つのアプローチがあったようだ。

一つは言語自体の設計を変えてしまう方法。これが現在の主流で、 C++ の源流となっている。

もう一つは、C のコンパイラー自体には大きな変更を加えず、文法的な表記に特殊仕様を入れ、パース時にこれを解釈して、オブジェクトを取り扱えるようにする方法。Objective-C は、どちらかといえば、こちらだろう。

だから、C++ 感覚で取り組もうとすると訳のわからないことになってしまう。

また、Objective-C がとっつきにくいのは、ビギナー向けのほとんどのサンプルが Mac や iPhone の GUI アプリだという点。これだと、Objective-C の勉強というよりも cocoa などの勉強になってしまい、Objective-C の言語の本質みたいなものが掴みにくい。

サンプル

というわけで、GUI 一切なしのコマンドラインプログラムを試しに書いてみた。


#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
#import <stdio.h>
// Person クラスの宣言
@interface Person : NSObject {
    NSString *name;
}
- (void)setName :(NSString *)Name;
- (void)printName;
@end
// Person クラスの実装
@implementation Person : NSObject
- (void)setName :(NSString *)Name {
    name = Name;
}
- (void)printName {
    printf("My name is %s \n", [name UTF8String]);
}
@end
// 実行プログラム
int main(void) {
    id person;
    person = [[Person alloc] init];//Person *person = new Person(); などとはしない
    [person setName:@"akiba"];
    [person printName];
    return 0;
}

これを Xcode 上で実行するとこんな感じになる。

Person クラスを実体化して、フィールド変数の name をセットした後、ゲッター経由でこの値を取得・表示するというよくあるプログラムだ。

GUI がないから、ワンファイルにすっきりと収まっているが、(Java や C++ に慣れた目から見ると)違和感を感じるところがちょいちょいある。

まず、どうということはないクラス Person でも NSObject を継承している点。またクラスを定義するときに@ディレクティブを多用している。

Objective-C の「C のコンパイラー自体には大きな変更を加えず、文法的な表記に特殊仕様を入れ、パース時にこれを解釈して、オブジェクトを取り扱えるようにする」という歴史的経緯に由来する特徴がよく出ているのではないかと思う。

ところで、最も違和感のあるのはこの箇所だろう。

    person = [[Person alloc] init];

オブジェクトに対してメッセージを送ることで機能を発現させているわけだが、これも歴史的な背景があり、 SmallTalk の影響らしい。

ただし、id 型は必ずしも使う必要はなく、

Person *person = [[Person alloc] init];

としても動きます。

また、Objective-C は C の拡張なので、C の関数も当然使える。実際、printf は C と同じように動作している。

その他、注意しておきたいところ

フィールド変数(インスタンス変数)

name のセッターは setName になってますが、フィールド変数の先頭文字を「大文字」にするのはお約束のようです。
ここら辺は Java と一緒でしょうか。

UTF8String がわからん、という声も聞きますが、name 自体は NSString (へのポインタ)です。標準 C の String に変換する必要があるので、ここで変換をかけているわけです。

なお、インスタンス変数の宣言の仕方などについては、この質疑応答が凄まじくよくまとまっています。

m と mm の違い

Objective-C のファイルは .m .mm の二つがあり得ます。
この記事がわかりやすいんですが、会員限定かな。
簡単にいえば、.m が objective-C ファイル本来の拡張子、.mm の方は C++ のファイルを取り込む際に使用する拡張子です。

継承とカテゴリ

オブジェクト指向の特徴の一つは継承だが、Objective-C はここら辺は素直だ。

よくわからんのは「カテゴリクラス」。

Person(hogehoge)

とあった場合、このクラスは元の Person のカテゴリクラスです。

元の場所とは違った場所でメソッドが追加できます。(同じ理由だが分割することもできる)

情報の古い(アップデートされてない)記事が多い

Obj-C ユーザーが少ない理由に、記事がそもそも少ないし、あっても古い(アップデートされてない)というのも一因になっていると思う。

例えば、よく「Obj-C ではクラス変数という概念がない」などと紹介されているが、2016 以降(OS 10.12 〜)はある

公式アナウンスはこちら

使い方は別記事で説明するかもしれないが、@property(class) などとする。
若干クセはあるのだが、普通にクラス変数っぽく使える。あるじゃん。

また、objective-c 自体も文法が(主に 2010 年代に)モダンに改変されており、@property や @synthesize なども使い方が微妙に変わっている。

この図はこちらの記事からお借りしてきました。わかりやすいですね、感謝!

 

多重継承の禁止とプロトコル

継承が出てきたので、書いておくと Obj-C では多重継承、つまり複数のクラスを親クラスにすることができないらしい。

多重継承の代わりにプロトコルで代用しなさいよ、というのがアップルの教え。
サンプルコードなどで

@interface ViewController : NSViewController<NSTableViewDataSource>

という書き方をよく見かけるが、<> 内がプロトコル。


このまま続けてもいいのだが、まとまり悪くなるのでここでいったん切ろう。

ワイもそうだが、IT 業界にいる限り Objective-C だけいじっていればいいというものではない。
というか実際に多い案件は、別の言語だったりする。

だから、この記事は「ちょっと objective-C 思い出すときに読み返す」ときに役にたつ内容にしておく。

その他

@property 表記

アクセサー(ゲッターやセッターのこと)を定義できる。慣れれば便利なのだろうが、Java あたり読み書きしているとすぐにこの規約を忘れる。

この記事読んでね。

クラスエクステンションの () 表記

忘れやすいのがクラスエクステンションの時に使われる () 。

詳しくはこの記事あたり参照。

なんでヘッダーで定義された宣言を .m ファイルでもう一回やる必要があるのか?疑問に思ったら読むといいと思う。

Person.h
#import <Foundation/Foundation.h>
@interface Person : NSObject
// 名前
@property (nonatomic) NSString *name;
// 名前と年齢をログに出力する
- (void)displayProfile;
@end
Person.m
#import <Foundation/Foundation.h>
@interface Person ()
// 年齢
@property (nonatomic) NSInteger age;
// 名前をログに出力する
- (void)displayName;
// 年齢をログに出力する
- (void)displayAge;
@end
@implementation Person
……
@end

要するに Obj-C には Public だの Private がないため、ヘッダーファイルではなく実体ファイルの方でメソッド・メンバー変数を定義することで外部クラスからのアクセス制限を行っているという理屈です。

Person.h
@interface Person: NSObject
@end

Person.m
@interface Person()
@end

@implementation Person
@end

という構造が見えてますか?

強調して書くとこういう仕組み。

強調して、と書いたがプロジェクトを作成するとき AppDelegate は () をつけて生成してくれますね。

 

 

 

 

 

 

カテゴリの () 表記

ここが紛らわしいと思うのだが、@interface () はクラスエクステンションだけでなくカテゴリでも使われる

ただ、こちらはファイル名自体が ClassA+ClassB.h のようになっているので、それで判断できるかな。

カテゴリーとクラスエクステンション』参照。