CS193p 課題6の内容

CS193pのクラスの公式サイトにLecture 14の課題としてAssignment6.pdfがアップされていますので、日本語にしておきます。

大量にヒントがあるので、ヒントを読むだけでも勉強になりそうです。データベースに関連する部分は特に手厚くヒントが書かれているので、データベースを扱った経験がない人にはかなり助かります。

 

課題の内容

目標

世界中の写真を見ることでバーチャルバケーションができるアプリを作成する。

主要な作業は、ユーザが吟味したり検索できるようにCore Dataを使ってバケーション単位に写真のデータベースを作成することである。

事前準備

CoreDataTableViewControllerクラスは提供されている。FlickrFetcherの新バージョンもアップされていて、今回の課題には必要である。
Flickr API Keyは今までどおり必要になる。

Required Task

このアプリでは、ユーザに世界の様々な場所のバーチャルバケーション先をコレクションしてもらう。ユーザは、行きたい場所の写真を選ぶためにこのアプリのFast Map Placesという写真選択機能を使う。このアプリには2つの主要課題がある:(1)ユーザに行きたい場所を選んでもらい、(2)ユーザにそのバーチャルバケーションのスポットでバケーションしてもらうことである。前者は写真が表示されているFast Map Placesの中に"Vist/Unvist"ボタンを追加することで達成される。後者は、タブバーコントローラに新しいタブを追加して、そのタブでユーザに場所または滞在希望地の写真のFlickr dictionary内にある検索タグでバーチャルバケーションをじっくり見てもらうことで達成される。

1. アプリに新しいタブを追加する。それは新しいテーブルビューコントローラがサンドボックス内のユーザのDocumentsディレクトリにある全ての”バーチャルバケーション”のリストを表示する。バーチャルバケーションファイルはUImanagedDocumentを保存することで作成される。(下記参照)各バケーションはDocumentsディレクトリ内に個別に自身のファイルを持つ。

2. ユーザがリストから1つのバーチャルバケーションを選択したら、ItineraryとTag Searchを選択する固定のテーブルビューが表示される。

3. Itineraryタブは、選択されたバーチャルバケーション内の写真が撮影された全ての場所のリストを(最初に訪問した場所を最初に表示するようにソートして)表示する。場所をクリックするとその場所で撮影されたバーチャルバケーションの写真を全て表示する。場所の名前は、photoInPlace:maxResults:メソッドで取得したFlickrの写真dictionaryを新しいFLICKER_PHOTO_PLACE_NAMEのキーで取得したものになる。この課題には新しいFlickrFetchrのコードを使う必要がある。場所の識別は、(FLICKR_PHOTO_PLACE_NAMEキーの戻り値の)場所の名前のみ使用する。(例えば、Flickrのplace idのようなものは無視する)

4. タグ検索タブは、ユーザがバーチャルバケーションの一部として選んだ全ての写真に付けられている全てのタグがリストアップされて(一番頻繁に組み見られているものが最初になるようにソートされて)表示される。タグをタッチすることにより、そのタグをもっているバーチャルバケーションの写真のリストが表示される。Flickrの写真dictionaryの中のタグはシングルスペースで区切られ全て小文字の1つの文字列の中にある。(FLICKR_TAGキーでアクセスできる)各タグを分割して抜き出し、ユーザインタフェイスの中で美しく見えるように頭文字を大文字にする。それらの中にコロンがあるタグを含めないこと。

5. ユーザに新規バーチャルバケーションを作成できるようにさせるUIは提供しなくてよい。(やりたければExtra Credit #1を参照) だから、コードでユーザのDocumentsディレクトリの中に1つの"My Vacation"という名前のバーチャルバケーションを作成すればよい。しかし、新しいバケーションを作成するUIを提供しなくてもよいというのは、他のRequired Taskについて複数のバーチャルバケーションをサポートしなくてよいということではない(Required Task #1のように)。残りのコードが適切に複数のバーチャルバケーションを処理出来ているかを確認するために、いくつか他のバーチャルバケーションを作ることになるだろう。

6. このアプリの全ての新規テーブルは、場所か写真かタグを表し、Core Dataで扱われる。必要なスキーマは自分で定義すること。

7. 全ての作業を行うために、もちろん、Visit/Unvisitボタンを写真を表示するストーリーボードの画面に追加する必要があるだろう。もし写真が既に"My Vacation"の中にあれば、その時はそのボタンのタイトルが"Unvisit"になり、さもなければ"Visit"になる。このボタンをクリックすることにより写真が"My Vacation"の一部なのかそうでないのかをトグルする。

8. これは両方のプラットフォームで動作する必要はない。2つのうち好きな方を選べば良い。しかし、1つ選んで、そのリアルなデバイス上で動作させること。だから、デバイスをどちらか選ぶこと。

ヒント

1. もし、UIManagedDocumentのインスタンスをディスクのdocumentごとに1つ持てばよく(各バケーションは自分のdocumentをディスク上に持つことを覚えておくこと)、アプリ全体で共有されるとしたらとても簡単だろう。(同じdocumentをUIManagedDocumentのインスタンスを使って複数回開くのは完全に正しいやり方であるけれど)

UIManagedDocumentのインスタンスを共有する方法はたくさんある。例えば、バケーションを与えると共用のUIManagedDocumentを返すメソッドだけを持つヘルパークラスを持つことである:

@interface VacationHelper
   + (UIManagedDocument *)sharedManagedDocumentForVacation:

(NSString *)vacationName;

@end

このケースでは、しかしながら、documentを使う全ての場所で適切にオープンされているかもしくは作成する必要があるかを確認しないとけないだろう。この結果、かなりの重複したコードが存在することになるだろう。

 

もしくは、それをハンドリングする前に必要ならオープンもしくはdocumentを作成するヘルパークラスを持つこのようなAPIを使うのも良いかもしれない。(しかし、オープンと作成は非同期に行われるので、このヘルパーメソッドはブロックを使ってコール元でハンドリングしなければならない)

typedef void (^completion_block_t)(UIManagedDocument *vacation);

 

@interface VacationHelper : NSObject
+ (void)openVacation:(NSString *)vacationName

usingBlock:(completion_block_t)completionBlock;

@end

与えられたバケーションに対してどのようにUIManagedDocumentを共有するかはあなたに任せるが、こうすることを強く勧める。

2. ヒント#1に従わずに同じdocumentでUIManagedDocumentのインスタンスを複数持つことが出来たなら、自分でそれらが同期するようにしなければならない。それは自動的には発生しない。だから、我々は、特定のdocumentを見る全てのコードがUIManagedDocumentの同じインスタンスを使うことを望む。

3. もしヒント#1の2番めのアプローチを使うなら、ブロックの講義で行ったメモリをリフレッシュするためにコードからブロックを実行させる方法を確認すること。(completionBlockのように)

4. contentsOfDirectoryAtURL:includingPropertiesForKeys:options:error:というNSFileManagerのメソッドが、ユーザが持っているバケーションを探し出すのに便利だと分かるかもしれない。(例え単に1つのバケーションを作成しようとしていても、複数のバケーションをサポートするようにコードを記述しなければならない)

5. (Required Task #1のテーブルから)ユーザが選んだバケーションを引き渡す方法について考えてみて欲しい。例えば、固定テーブルビューはその情報を保有してさらにsegue先のコントローラに渡さなければならなくなるだろう。

6. 全てのNSManagedObjectは、自分がどのNSManagedObjectContextの中にいるかを知っている(NSManagedObjectのmanagedObjectContextプロパティを通してアクセス可能である)ことを思い出して欲しい。

7. NSManagedObjectContextはスレッドセーフではない!NSManagedObjectContextが作られたスレッドの中で、もしくはNSManagedObjectContext(もしくはそのUIManagedDocument)のperformBlock:かperformBlockAndWait:メソッドを使ってアクセスする(それによってブロックがそのコンテキストの正しいスレッドで処理される、それはメインスレッドになるだろうが)。この課題でそれについては何もする必要はない。(全てのCore Dateのアクセスには単にメインスレッドを使えば良い)

8.  @"My Vacation"の文字列はアプリのコード上に1行だけしか現れないこと(Visitボタンを実装する場所に)。言い換えれば、他の場所にハードコードしないこと。残りの実装はバケーションのdocumentに関して汎用的であること。

9. バケーション内の場所のテーブルは最初に訪れた場所が先頭になるようソートされなければならない。そこで、場所が最初にデータベースに追加された時に、それを記録する属性を持つ必要があるだろう。

10. さらに、あるタグを持っている写真の数を追跡する属性がデータベースのどこかに必要になるだろう。(検索タグとそのタグを持つ写真の間にto-manyのリレーションを持っていれば計算するのは簡単だろう。この属性を新しい写真がバケーションに追加された時と写真が削除("unbisited")された時の両方で更新する必要があるだろう。

11. バケーション内の任意の場所の写真のリストは、どんな方法でもよいのでソート可能にする。

12. エンティティの属性の取得と設定にその方が良ければvalueForKey;/setValueForKeyを使っても良いが、講義であったようにNSManagedObjectのカスタムサブクラスを作成してエンティティ固有のObjective-Cのカテゴリを追加することになるだろう。これはオブジェクトの作成する時(オブジェクト間のリレーションを適切に設定したくなるだろうから)や削除する時(prepareForDeletionの中でオブジェクト間の全てのリレーションを削除したくなるだろうから)には特にこうするのが良い。

13. データベースにオブジェクトを挿入する時に、依存関係のある他のオブジェクトを挿入し、それらの間のリレーションを設定する(単に保有しているそのプロパティに値を割り当てる)のはいいタイミングである。このアプリがデータベースに行う変更は、("visiting"によって)写真を追加することであるが、明らかに他の多くのオブジェクトがその時にデータベースの中に作成されるだろう。全てのデータベースの変更をそのメソッド内に集めればかなりクリーンになるだろう。

14. データベースからオブジェクトを削除する時、そのstrongポインタを持ったままにしないよう注意すること。(削除した後は手元になくなってしまうので)

15. データモデルの中にdescriptionという属性の名前は使わないこと。これは問題を引き起こす。(NSObjectのdescriptionメソッドのせいで)

16. データモデルの中にidという名前の属性を使わないこと。これがObjective-Cで引き起こす問題を想像できるだろう。

17. NSManagedObjectをNSLog()するのは有用だ。それは多くの詳細情報を見せてくれる。(データベースに由来する問題の場合)

18. タグや写真のような決まったタイプのオブジェクトのリストを取得するためには、Xcodeのデータモデルで作成したオブジェクトモデルの中に、そのオブジェクト(エンティティ)を表すものが必要だということを忘れずに。もしくは、データベースの中の言葉で言えば、そのテーブルがなければならない。例えば、データベースの中に写真を表すエンティティがなければNSFetchedResultsControllerを使ってテーブルビューの中に写真のリストを取得することは出来ない。

19. オブジェクトモデルを変更するなら、古いデータベースが削除されるように実行する前にデバイスやシミュレータからアプリを削除すること。
これをするには、ホーム画面でアプリのアイコンを押したままにし、それが揺れだしたらアイコンの角に出てくるXを押す。スキーマを変更した時にこれをしなければ、プログラムはクラッシュする。(データベースのdescriptionに互換性がないというメッセージがコンソールにでる)

20. saveToURL:forSaveOperation:completionHandler: を使ってバケーションのdocumentに加えた変更を保存すること。おそらく、Visitボタンの実装の中でのみdocumentを変更しているだろう。

21. モデルの中に双方向のリレーションがある場合、片側を設定すると自動的に他方が適切に設定される。

22. "To many"のリレーションは、NSManagedObjectインスタンスのNSSetとしてシンプルに表現される。NSManagedObjectのサブクラスを作る場合(強く推奨)、そのNSSetのmutableCopyを作成して操作することが出来る(そしてそのコピーしたものにプロパティをセットする)、もしくはそのセットにオブジェクトを追加したり削除したりするための便利なメソッドがある。

23. この課題で提供されているCoreDataTableViewControllerを使う必要はないが、自分がやりたいものはほぼ似たようなものになる。その実装はほとんどが単にNSFetchResultsControllerのドキュメントからコピー&ペーストしたものである。サブクラスにして使うのはとても簡単である。それを動作させるためにしなければならないことは、fetchedResultsControllerのプロパティをセットすることだけである。
UITableViewDataSourceメソッドで実装が必須なのは、tableView:cellForRowAtIndexPath;である。tableView:didSelectRowAtIndexPath:やprepareForSegue:sender:も実装したくなるだろう。

それらを実装するのにself.fetchedResultsControllerのobjectAtIndexPath:メソッドを使うのは素晴らしい。

24. (バケーションのdocumentを保存することによって)Core Dataのデータベースを保存する時はいつでも、NSFetchedResultsControllerを使えばなにもしなくてもユーザインタフェイス内で自動的に更新が行われることを忘れないように。(CoreDataTableViewControllerがそうするように)
これはNSFetchedResultsControllerとCore Dataのキーバリュー監視メカニズムの驚異である。
NSFetchedResultsControllerを使わずにテーブルビューを実装しようとしたら、自分の力でテーブルが更新されたことを知る方法を実装しなければいけない。

25. テーブルビューを動作させるために大量のコードが必要になったら、間違った方向に向かっている。NSFetchedResultsControllerに全てのことを任せよう。(CoreDataTableViewControllerを使って)

26. CoreDataTableViewControllerベースのビューコントローラの中でのソートは、作成したNSFetchedResultControllerの一部として自動的に行われる。NSSortDescriptorを正しく設定すること。(そしてデータベーススキーマの正しい属性を設定すること)

27. ユーザがバケーションにいる時は、Recentタブは更新する必要はない。言い換えれば、その部分は前回の課題のままのコードでよい。

28. 前回の課題から画像を表示するMVCは、いくらかアップグレードする必要がある。Flickrの写真dictionaryを元に表示するのに加えて(バケーション中のユーザ用に)データベースを元に写真を表示出来るようにしなければならない。そして、Visitボタンを含むシーンをサポートできなければならない。ここで優秀なオブジェクト指向のテクニックを使うことを強く勧める。例えば、Visitボタンをハンドリングするだけのクラスが必要になるだろう。(そしてそれは写真を表示するために必要な残りの振る舞いを継承するだろう)

29. ユーザがバケーション中に写真を見ている際にUnvisitボタンを押したケースに注意すること。iPadの画像表示をクリアする必要があるかもしれない。というのもデータベースから写真を削除してしまってもう一度Visitすることができなくなるからだ。そしてiPhoneでは、このケースはナビゲーションコントローラで元に戻ることになるだろう。

<前の記事

CS193p - Lecture 14             - 次の記事>