Study CoreData 4 ~石橋を叩かず渡って落ちました(泣)~

みなさん、こんにちは!
お元気ですか?
 
今回で『Study CoreData』シリーズも第5回目になりました。
みなさん、なんとなく理解でき始めてきた感じでしょうか?
僕もつまづきながらも、ちょっとづつですが使えそうな気になってきました。
 
まだまだ先は長くなりそうですが、あまり構えず進んでいくことにしましょうね。
 
では、さっそくはじめていきましょう。

注意:投稿者自身もCoraDataについて勉強中のため、このシリーズには誤りが含まれている可能性があります。もし、間違いに気付かれた方はコメント欄もしくはtwitterなどでご指摘いただけると幸いです<(_ _)>
また、開発環境はXcode3.2.3 iPhone SDK 4です。実機でのテストなどは自己責任でお願いいたします。

 
 
1. 新しいデータオブジェクトのつくりかた
 
前回、データの設計図を作りましたので、今回はそれを使って大胆にオリジナルのアプリの作成をしていきましょう!!
 
…と言いたいところですが、実際ここでそれを始めるとエライコトになります(>〜<;)
(なぜかと言うと、僕がそうなったからです…^^;)
ですので、もう少し用意されたコードを見ていくことにしましょう。
 
 
以前の第2〜3回で、<NSManagedObject>の取り出しまでは何となく分かりましたが、まだそれをどう使って、どう編集(追加や削除)し保存するのかはまだ調べていませんでしたね。
なので、まずはそこら辺を見ていきます。
 
このデフォルトの状態でユーザがまずやる事は『データを追加する』ってことです(ってかそれしか出来ません)ので、まずはデータの追加から。
 
前提として、
RootViewControllerは、appDelegateから渡された<NSManagedObjectContext>(データ状態の記憶)を持っていて、
さらにそれを元に自身で用意した<NSFetchedResultsController>も持っている

事は憶えておきましょう。
 
 
改めてデータモデリングツール内の<Event>エンティティを見てみると…

このように持っているプロパティは属性であるtimeStamp(NSDate型)だけで、他への関連などもないことが分かります。
 
 
そして、データ追加用のaddButtonのアクションは[insertNewObject:]というメソッドに送られていますので、そこで何をしているのかを見てみます。
 
 
この中に、

[newManagedObject setValue:[NSDate date] forKey:@"timeStamp"];

と、

if (![context save:&error])

というコードがありますね。
 
ってことは、ここで新しいデータオブジェクトの属性(timeStamp)に値([NSDate date])が入れられて、<context>に保存されているって感じなのかな?
ってことが予想できます。
 
で、最初のブロックは新しいデータオブジェクトの作成のようです。

NSManagedObjectContext *context =
 [self.fetchedResultsController managedObjectContext];

 NSEntityDescription *entity =
  [[self.fetchedResultsController fetchRequest] entity];

NSManagedObject *newManagedObject = 
 [NSEntityDescription insertNewObjectForEntityForName:[entity name] 
		          inManagedObjectContext:context];

最後のnewManagedObjectのところを見ると、
エンティティの名前と<NSManagedObjectContext>があれば新しいデータオブジェクトが作れる事が分かりますね。
そのためのコンテキストとエンティティの取り出しを上の二行でやっています。

1. fetchedResultsControllerに入っているmanagedObjectContextを取り出す。
2. fetchedResultsControllerに入っているfetchRequestからentityを取り出す。

簡単ですね。
 
 
ちなみに、self.fetchedResultsControllerの中にあるコンテキストは、appDelegateから渡されたものだし、エンティティの名前(Event)は分かっているので、上の二行をはぶいてこんな感じでも同じデータモデルを作れます。

NSManagedObject *neoManagedObject = 
 [NSEntityDescription 
	insertNewObjectForEntityForName:@"Event" 
	    inManagedObjectContext:[self managedObjectContext]];

 
その次のキー値コーディングでの値のセットは、こんな感じにも出来ますね。

neoManagedObject.timeStamp = [NSDate date];

 
『…え?…ナンデ!?』

っと思った方、正解です!!
今の状態では、この書き方は出来ません。
ですが、ある事をすると可能になります。それはまた今後。
 
 
で、最後の部分が保存処理ですね。
<NSManagedObjectContext>に保存するわけです。
だから、このコンテキストがデータの状態を記憶できるのか!
で、その保存が失敗した場合にはif内の処理を実行するようですが、基本的にその前までに出来る限りエラーが起きない状態にしておくのは当然ですね。
 
 
さ、そこまで分かったら、久しぶりにこのプロジェクトをビルドして、追加ボタンを押してみましょう。
(まだ何も変えていないので以前見たのと変わりませんが)
もし、エラーが出たり起動しない場合は、一度シミュレータから削除して再ビルドしてみてください。
 
 
そこで、実際にやってみると、追加ボタンから呼ばれていたメソッド内では新しいデータオブジェクトの作成と値の代入と保存だけしかしていなかったのに、ちゃっかりテーブルビューの更新([tableview reloadData])やアニメーションしながらの行の移動なんかしちゃってることに気付きます。
 
んぢゃ、これはどうなっているんでしょう?
また、コードに戻ってみてみます。
 
 
2. 呼んでないのに…。

今度は、同じRootViewController.m内
#pragma mark Fetched results controller delegateってブロックを見てみることにしましょう。

やたらと長いメソッド名なものもありますが、そこはおいといて。
(ちなみに<NSFetchedResultsController>の部分を⌘+ダブルクリックすると、このクラスのヘッダファイルを見れますのでより詳しく調べたい場合はそこから情報を得る方法もあります。)
 
ここで使われているのが以下の4つですね。

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller;

- (void)controller:(NSFetchedResultsController *)controller
   didChangeSection:
     (id <NSFetchedResultsSectionInfo>)sectionInfo
               atIndex:(NSUInteger)sectionIndex
    forChangeType:(NSFetchedResultsChangeType)type;

- (void)controller:(NSFetchedResultsController *)controller 
  didChangeObject:(id)anObject
         atIndexPath:(NSIndexPath *)indexPath 
   forChangeType:(NSFetchedResultsChangeType)type
        newIndexPath:(NSIndexPath *)newIndexPath;

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller;

 
上から順に一部分を抜き出すと、

…willChangeContent…
…didChangeSection…
…didChangeObject…
…didChangeContent…

となっていますね。
 
これってUIViewやCoreAnimationで使われる、
[animationDidStart]や[animationDidStop:]の延長のように考えると分かりやすいんじゃないでしょうか?
 
つまり、Contentが変わる前と後セクションが変更された後オブジェクトが変更された後にそれぞれのメソッドに自動的に通知される仕組みになっているようです。
さらに、didChangeSection/didChangeObjectの中のswitch文を見ると、

NSFetchedResultsChangeInsert
NSFetchedResultsChangeDelete
NSFetchedResultsChangeUpdate
NSFetchedResultsChangeMove

とあるように
挿入/削除/内容の変更/移動、それぞれに対応した処理を設定している事が分かります。
 
例えば、didChangeObject の NSFetchedResultsChangeMoveなら、
現在のindexPathの行を削除して、新しいindexPathの行を挿入する
って感じの処理が書かれていますね。
 
 
じゃあ、残りのwillChangeContent/didChangeContentは何をしてるか見てみると、
[self.tableView beginUpdates]

[self.tableView endUpdates]
 
つまり、
『これからアップデートするよ〜』
『アップデート終わったよん』

ってTableViewに通知してるんですね。
 
ってことは、表示を変える前にしたい処理はwillChangeContentに、
表示が変更された後にしたい処理はdidChangeContentに書けば良い
ってことみたいです。
これ、憶えておくといつか役に立つかも?
 
 
これで、インターフェイス上の表示と実際のデータの同期(同じ状態にする)方法は分かりました。
 
あと気になるのは、そのデータを
いつ、どのタイミングで保存しているのか?
ってこと。
それを知っておいた方が良さそうですね。
 
 
3. データを保存するタイミング
 
⌘+シフト+Fキーを押して<save:&error>という文字列(<>は除く)をプロジェクト内検索してみてください。
 
すると、AppDelegate.mにひとつ、RootViewController.mにふたつある事が分かりますね。
つまり、データが保存されるタイミングが3つ設定されているワケです。
 
では、それがそれぞれどのタイミングなのかを見てみましょう。
 
まずは、AppDelegate.mから。
これは、[applicationWillTerminate:]に書かれていますね。
つまり、アプリ終了時に保存されています。
 
if文を見てみると『managedObjectContextが変更されている場合だけ保存する』ようです。
つまり、データの変更がされていないのであればモーマンタイ(無問題…古っっ!)でスルーってコトみたい。
 
 
次の保存場所はRootViewController.mの[insertNewObject]ですが、ここは前述したので割愛です。
 
んで、最後が同じViewController内の[tableView: commitEditingStyle: forRowAtIndexPath:]
つまり、
テーブルビューセルのEdit時に削除/移動されたタイミングの処理ですね。

ここでは、セルが削除された場合の処理だけが書かれています。
 
<NSFetchedResultsController>からコンテキストを取り出して、削除されたセルのindexPathに対応していたデータオブジェクトを削除してから保存しているみたいですね。
 
つまり、別の方法でセルやデータオブジェクトの削除を実装したい時には、ここと同じようにそのオブジェクトをコンテキストから削除してから保存してあげなければいけないってコトですね。
 
 
順を追ってまとめてみるとデータを保存するタイミングは…

1. 新しいデータオブジェクト<NSManagedObject>を作成した後。
2. アプリの表示上でデータが削除された(コンテキストにはまだ残っている)後。
3. アプリ終了時。

となってます。
 
ただ、1.に関しては[insertNewObject]の中で、新規オブジェクトの作成と値の代入が行われているので、
空っぽのデータオブジェクトを作成した直後なのか?
それともそのオブジェクトに値を入れた後なのか?
またはその両方なのか?ってところははっきり分かりませんね。
 
また、その辺りはおいおい見ていくコトにしておきましょう。
 
 
…と、早足で見てみましたが、実は今までのものも含めて、これでデフォルトで生成されたコードのおよそ9割をすべて見終わったコトになります!
 
みなさま、おめでとうございます!! そしてオレ…
 
 
まさか、5回のうち3回のエントリを全部使って、やっと初歩的なコードの解読が終わるとは…
 
…恐るべし虎亜出餌多(CoreData)…
 
ってことで、今までのおさらいを^^;
 
 
4. これまでのあらすじ
 
第0回では、CoreDataを始めるための準備をしてみました。

IB関係のファイルを削除したり、静的アナライザをONにしてみたり…。
まぁ、でも実際の開発にIBを使うか使わないかは個人の好みだったりしますんで、絶対的にどっちらかが良いってコトも無いかと。
(僕の場合は、IBでの接続ミスとコード上のミスを別々に探すのが面倒だったり、動的な処理の比重が高かったりするのでIBは全く使ってません。)
 
そして第1〜2回では、Xcodeが勝手に書いてくれたコードを見ながら、CoreDataのデータがどういう流れで表示されるのかを探ってみましたね。
 
キー値コーディング(KVC)に関しても、ちょこっとだけ説明させてもらいました。
 
 
その流れをもう一度整理して、補足も加えもうすこし分かりやすくまとめると…

AppDelegate内で
1. <NSManagedObjectModel>(データ全体の設計図)を取得。
2. その設計図に加えて、保存先(TodoCore.sqlite)へのパスとデータベースのタイプなどが設定された<NSPersistentStoreCoordinator>(ハードディスクに記憶するための管理をする)のインスタンスを作成。
3. その<NSPersistentStoreCoordinator>をセットした<NSManagedObjectContext>(データの状態を記憶している)のインスタンスを作成し、RootViewControllerに渡す。
 
RootViewControllerでは、
4. <NSFetchedResultsController>を使う事でUITableViewControllerでの処理が便利になる!(らしい)
5. それを生成するために必要な<NSFetchRequest>(データを取り出す条件をとりまとめる)を用意する。
6. <NSManagedObjectContext>からエンティティ(単一のデータの設計図)名を指定して<NSEntityDescription>(エンティティ)を取り出して5.に渡す。
7. さらにソート条件を指定した<NSSortDescriptor>を<NSArray>へ入れて5.に渡す。
     (※他に述語<NSPredicate>と呼ばれる条件式も渡すことができます。)
8. そうして出来上がった<NSFetchRequest>と大元の<NSManagedObjectContext>、さらにテーブルビューのSectionに指定するエンティティの属性の名前(任意)を元に<NSFetchedResultsController>のインスタンスを作成。
9. [- (BOOL)performFetch:]というメソッドとエラー処理を記述する事で、<NSFetchedResultsController>が使用可能になる。
10. そして、その<NSFetchedResultsController>のインスタンスにテーブルビューのindexPathを渡す事で、そのindexPathに対応したデータ<NSManagedObject>が取得できる。
 
(つまり、
<NSManagedObjectContext>には<NSPersistentStoreCoordinator>、<NSManagedObjectModel>、<NSEntityDescription>などが入っていて、
<NSFetchRequest>には、<NSEntityDescription>、<NSSortDescriptor>、<NSPredicate>が入っていて、
<NSFetchedResultsController>には全部入ってる!!ってことみたい。)

以上!
(ってか、簡単にまとめても10項目って…ε=( ̄。 ̄;))
 
 
そして、
第3回では、上のまとめの10.で取り出した<NSManagedObject>の元になる<NSManagedObjectModel>(データの設計図)を、データモデリングツールを使って実際に作成してみました。
 
その際に、エンティティやそれに含まれるプロパティである属性や他のエンティティとの関連、さらに削除ルールなどについても勉強しました。
 
 
今回はデータオブジェクトの作り方と<NSFetchedResultsControllerDelegate>のメソッドを使ったUITableViewの更新、さらにデータ保存のタイミングを調べてみました。
 
 
…って流れで、次回はやっと実用アプリのための実装に入っていきます!
つまり実際にコードを変更していくわけです。
 
なので、これまでの内容(第1回は除く)を理解していないと、多分何をやっているのかワケが分からなくなってしまうと思います。
(そのためのまとめでした。)
 
もし、途中で『あれ?これって何だっけ??』な感じになったら、無理矢理進めずに引き返してみる事をオススメします。
 
 
と、そんなこんなで今日はここまでにしときます。
 
今回はちょっとマジメすぎましたかねぇ?
以後気をつけますっ!
 
 
 
それでわ。
ばいちゃっ!!

コメントを残す