CS193pのLecture 14の課題6ができました。ふぅ、きつかった…。
最初にどんなことをするアプリなのかを把握するのに時間がかかりました。
データベースにいろんな写真の情報を登録するようだけど、その写真はどこから持ってきたものなのかとか…。
そして、今までのアプリの機能を使って写真を見ながらVisitボタンでお気に入り登録して、それが自動的にタグとか場所単位に分かれている感じなのだと理解しました。
今までの機能も合わせて、最終的に(1)ユーザデフォルトと(2)ファイルシステムと(3)Core Dataという3つの永続性の仕組みが上手く使われた快適なアプリになりました。
はまったこと
ブロックやらCore Dataやらカテゴリやら分からないことが多くてヒントを1つクリアする度につまずいて、という感じでした…。
Documentsディレクトリに存在するデータベースの取得
ファイルシステム上のファイルがデータベースかどうかの確認方法が分からなかったので、ずるしてサンドボックスのDocmuments内にあるディレクリはデータベースとして判断するようにしました。
具体的には、NSFileManagerのcontentsOfDirectoryAtURL:includingPropertiesForKeys:options:error:メソッドで、includingPropertiesForKeysにNSURLIsDirectoryKeyを設定して使用しました。
戻り値がNSURLの配列になっていて、その各オブジェクトに対してresourceValuesForKeys:err:メソッドを送信するとdictionaryが返ってくるので、そのdictionaryからキーとして先に指定したNSURLIsDirectoryKeyを指定して値をとりだし、BOOL値に直してその値からディレクトリかどうかを評価しました。
ずるしてディレクトリかどうかで判断してるだけなのに、とてつもなくめんどくさいです…。
データベースをオープンする場所
始めはバケーションの一覧(データベースの一覧)からバケーションが選択された時にデータベースをオープンしようとしていましたが、画面遷移のほうが先に終わってしまうので、画面だけ変わってデータベースへのポインタが引き渡せない状態になってしまいました。
画面遷移時に引き渡すのはデータベース名だけにして、データベースのオープンは使用する直前にするようにしました。
UIManagedDocumentの共用
ブロックを型定義したメソッドの作り方にかなり悩みましたが、これで動くようになりました。
VacationHelper.h
#import <Foundation/Foundation.h>
typedef void (^completion_block_t)(UIManagedDocument *vacation);
@interface VacationHelper : NSObject
+ (void)openVacation:(NSString *)vacationName
usingBlock:(completion_block_t)completionBlock;
@end
VacationHelper.m
#import "VacationHelper.h"
@implementation VacationHelper
+(void)openVacation:(NSString *)vacationName
usingBlock:(completion_block_t)completionBlock
{
NSFileManager *fm=[NSFileManager defaultManager];
NSURL *docDirURL=[[fm URLsForDirectory:NSDocumentDirectory
inDomains:NSUserDomainMask] lastObject];
NSURL *dbfile=
[docDirURL URLByAppendingPathComponent:vacationName];
UIManagedDocument *database=
[[UIManagedDocument alloc]initWithFileURL:dbfile];
if (![fm fileExistsAtPath:[dbfile path]] ) {
//ファイルが存在しない場合
[database saveToURL:dbfile
forSaveOperation:UIDocumentSaveForCreating
completionHandler:^(BOOL success) {
if (success) completionBlock(database);
if (!success) NSLog(@"couldn't create document at %@",dbfile);
}];
}else if (database.documentState == UIDocumentStateClosed){
//ファイルがクローズしている場合
[database openWithCompletionHandler:^(BOOL success) {
if (success) completionBlock(database);
if (!success) NSLog(@"couldn't open document at %@",dbfile);
}];
}else if (database.documentState == UIDocumentStateNormal){
//ファイルがオープンしている場合
;
}
}
@end
このメソッドを使用する時は、メモリサイクルにならないように、クラスのプロパティをブロック内で参照する部分は__weakを使ってweak参照にしてからアクセスしています。
__weak ImagePresenterVC *weakSelf = self;
[VacationHelper openVacation:self.vacation
usingBlock:^(UIManagedDocument *vacation) {
weakSelf.database = vacation;
if ([Photo
photoInManagedObjectContext:vacation.managedObjectContext
photoId:weakSelf.photoId]) {
weakSelf.photoExsitInDb = YES;
weakSelf.VisitUnvisitButton.title = @"Unisit";
}else{
weakSelf.photoExsitInDb = NO;
weakSelf.VisitUnvisitButton.title = @"Visit";
}
weakSelf.photoTitle =
[Photo photoInManagedObjectContext:vacation.managedObjectContext
photoId:weakSelf.photoId].title;
}];
出来てみると、使い方も簡単でコード全体がすっきりしています。
かなり驚きました。とても便利です!
Core Dataのデータを見る方法
作っている最中にデータが正常に入っているかどうかCore Dataの中身を見たくなりグーグルで見る方法を調べました。
この間調べたように、Core Dataの中身はSQLiteのデータベースなので、SQLiteのデータメンテナンスができるアプリを探してみました。
どうも定番アプリはないようで、次のようなアプリをはじめ他にもたくさんありそうでした。
- Lita
- sqlbudy
- SQLite Database Brower
そしてちょっと使ってみたりしたのですが、今回はそれほど複雑なデータ構造ではないので、結局ターミナルで次のように確認しました。
$ SQLliteのファイルがあるディレクトリに移動
$ sqlite3 persistentStore
sqlite> .tables
ZPHOTO ZTAG Z_METADATA
ZPLACE Z_1TAGED Z_PRIMARYKEY
sqlite> select * from ZPHOTO;
persistentStoreというのがSQLiteのファイルです
.tablesコマンドでテーブル一覧が表示されます
Z_で始まるのは身に覚えがないのできっとシステムのテーブルでしょう
今回作成したのはPhoto, Place, Tagの3つのエンティティです
Core DataでTo-manyリレーションのデータを取得
タグ一覧画面から写真一覧画面に遷移する時ですが、タグ名を元に写真のデータを取得する必要があり、そのリレーションがTo-Mamyでやり方が分からず悩みました。
To-Manyのリレーション先の属性で条件を絞る場合は、次のように条件文を指定するようです。
predicateWithFormat:@"any taged.tagName = %@", tag.tagName];
写真{リレーション:taged} (To-Many)-------->> タグ{属性:tagName}
写真エンティティから上記のpredicate指定でデータ取得
Core Dataからのデータ取得で失敗
executeFetchRequest:error:メソッドを使ってデータを検索すると、戻り値がnilでerrorにも何もセットされずnilでした。
この原因は、バグでDBがオープンされていない状態でこのメソッドを使用してしまったからでした。つまり、このメソッドを送った先のNSManagedObjectContextのインスタンスがnilでした。
Core Dataのデータの削除
リレーションは自動的に削除されますが、リレーション先のデータは自分で削除する必要がありました。
Core Dataのデータ削除後に一覧画面に戻った時の表示
Unvisitボタンで写真削除後にリスト画面に戻って来ると削除内容が反映されません。
[self.tableView reloadData]
とかやってみましたけど、ダメでした。
削除内容をテーブルビューに反映させるには(segue先から戻って来た時にCore Dataの変更内容をテーブルビューに反映させるには)、提供されたテーブルビューコントローラ(CoreDataTableViewController)のfetchedResultsController に、NSFetchedResultsControllerのインスタンスを設定しないといけないようです。
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (self.database) {
[self setupFetchedResultController]; // fetchedResultsControllerの設定
}
}
storyboard
場所一覧もしくはタグ一覧から先の部分は全ての写真を表示するとしか説明がなかったのですが、テーブルビューで一覧表示しました。
<前の記事 |
CS193p 課題6の内容 | - | CS193p - Lecture 15 | 次の記事> |
コメントをお書きください
はん (金曜日, 16 12月 2011 14:04)
こんにちわ。
質問ありますが、今回の課題でVacation毎にDocumentファイルを作成するって書いてあったと思いますけど、
となるとVacation毎にDatabaseが作られる感じですか?
good-morning-call (金曜日, 16 12月 2011 15:38)
はんさんこんにちは〜。
ヒントにNSFileManagerのメソッドを使ってバケーションをリストアップするようなことが書いてあるので、おっしゃるとおり、私もVacation毎にDatabaseが作られる感じだと思います。
また、ヒントにはポインタを変えれば1つのデータベースで別のNSManagedContextを持てるとも書いてありましたが、あまりお勧めではないような記述だったので私はその方法はやっていません。
ちなみに、今回の課題ではユーザが自分で任意のVacation(データベース)を作成する機能がないので、アプリを動かすと固定で"My Vacation"というデータベースが1つだけ出来ました (*´∀`*)
はん (金曜日, 16 12月 2011 16:49)
レスありがとうございます。
なるほど。なぞ解決ですw