Study CoreData 18 ~時には強引さも必要?~

みなさま、暑い夏いかがおすごしでしょうか?

僕は2週間ほど前、蛍を散策にとある湖畔にメイッコたちと一緒にちょっとしたお出かけをしてきたんですが、残念ながら時期がちょっと遅かったらしく蛍を見る事は出来ませんでした…(>〜<;)
(時期によっては感動するくらいたくさんの蛍が見れるんですよぉ!)

ですが、明るい時間帯にそこに行ったのは初めてで
『まだこんなに水がキレイな場所があったんだ!』
って思うくらい良い場所だなぁと感じました。

 
 
家族や友達連れでキャンプやバーベキューなんかするにはとっても良い場所だと思うので、ご興味のある方はこちらを。(コテージや釣り堀もありましたよ!)
ウェルキャンプ西丹沢
 
 
…っと、ちょっと夏らしいオープニングでしたが
今日もあっつく更新していきます!

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

 
 

1. Relationshipの削除

前回はRelationshipの追加とリスト表示を実装したので、今日は追加したRelationshipの削除から実装していきたいと思います。
そのために今回はこんな仕様にしてみようかと。

・削除されようとするセルが[task]に関連していない場合はそのまま削除。
・[task]への関連を持っている場合は[tasks]の数をアラートビューで表示。

・Categoryの場合…
 ・編集中の[task]が関連している[category]は削除不可に。
 ・別の[task]への関連を持っているなら、同時に削除される事をアラートで表示。

・Tagの場合…
 ・編集中の[task]が関連している[tag]でも削除可能。

“Categoryの場合…”の箇所ですが、
ある[category]が削除されると関連する[task]も削除される(カスケード)ように
設定してあるためです。
特に、編集中の[task]が関連している[category]を削除出来るようにしてしまうと、編集中の[task]まで削除されてしまうので予期せぬエラーの発生を防ぐために“削除不可”にしておきます。
 
 
で、セルの削除を実装するのはSelectableViewControllerの[commitEditingStyle:]ですね。
こう書き換えてみました。

ここではまず、”削除時の処理”として削除するオブジェクトを取得。
そしてそのオブジェクトが持っている[tasks]の数が0かどうか?によって分岐します。

ここで使っている[_waitDeletePath]ですが、これは『削除予定のセルのindexPathを一時的に入れておく』もので“NSIndexPath”のインスタンス変数として宣言してます。

もし、countが0でなければアラートを表示して[_waitDeletePath]に現在のindexPathを入れ、[return]でこのメソッドを抜けます。

countが0の場合はコンテキストからオブジェクトを削除してデータを更新、最後にテーブルビューからセルを削除して終わります。
(ついでにセル挿入時のアニメーションも加えておきました。)
 
 
…で、”count != 0″でアラートを表示してメソッドを抜けた後はこうします。


// アラート処理
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
	
	if (buttonIndex != alertView.cancelButtonIndex) {
		
		// [_waitDeletePath]を使って元のメソッドへ戻す。
		[self tableView:self.tableView commitEditingStyle:UITableViewCellEditingStyleDelete 
	  forRowAtIndexPath:_waitDeletePath];
	}
	// キャンセルなら何もしない。
}

 
こうすることで[commitEditingStyle:]内の分岐でアラート表示部分を通らずに、オブジェクトの削除とデータの更新が実行されます。
 
 
さらに、編集中の[task]に選択されている[category]を削除不可にするために
[editingStyleForRowAtIndexPath:]をオーバーライドします。

 
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView 
		   editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath {
	
	if (_editType == EditTypeCategory && [tableView cellForRowAtIndexPath:indexPath].accessoryType != 0) {
		
		UIAlertView *alertView = [[[UIAlertView alloc] initWithTitle:@"選択されているカテゴリーは削除できません。" 
															 message:nil delegate:nil 
												   cancelButtonTitle:@"OK"  otherButtonTitles:nil, nil] autorelease];
		[alertView show];
		
		return UITableViewCellEditingStyleNone;
	}
	return UITableViewCellEditingStyleDelete;
}

 
ここでのポイントは
“[tableView cellForRowAtIndexPath:indexPath].accessoryType != 0”
そのセルにアクセサリ(チェックマーク)が付いているかを調べているところと、
“アラートビューのdelegateをnil”にして、
ボタン押下時にはアクションを実行させないようにしている点ですかね。
 
 
では、これでRelationshipの削除は実装できたはずなので、ビルドして確認してみてくださいね〜。
 
 

2. “ユーザビリティ改善用ボタン”を使った画面遷移と保存処理

どうでしょう?大丈夫そうですか?
ちゃんと削除が出来たら次へ行きましょう!

お次ぎは『TodoCore』でもやった
[AddDetails/Save & Show List]ボタンのアクションの設定です。
内容的にはTodoCoreでの実装と大きくは変わりませんが、前回よりちょっとスッキリとさせたいと思います。

そのためにまず、
遷移元のViewControllerがModalViewを持っているかどうか?
を把握しておくための値を用意しておくことにします。
 
 
ってコトで、今回は[EditValueController]で使っている構造体[_editingInfo]にそいつを加えてみましょう。

	struct {
		unsigned int cellStyle:3;
		unsigned int canMaltipleSelect:1;
		unsigned int showKeybord:1;
		unsigned int hasModalViewController:1;
    } _editingInfo;

5行目の[hasModalViewController]がそれです。

そしたら、これに値を入れておきます。

このように[managedObject isInserted]とすることで
“そのオブジェクトはコンテキストに挿入されたものかどうか?”が返されます。

RootViewControllerの[addNewTask]を見てもらうと分かるように、
[showModalAddTitleView]からの場合はコンテクストにinsertされていますが、
[didSelectRowAt…]からの遷移の場合は”NSManagedObjectID”を使ってtaskを取得しているのでinsertされていないってワケですね。
 
 
そこまで出来たら、EditableViewControllerに移動して
[AddDetails/Save & Show List]ボタンにもアクションを加えましょう。
加えるアクションメソッドは、Cancel/Saveボタンのアクションメソッドと同じ[judgeButtonTapped]です。

 
 
次に画面遷移処理を変更します。
[if ([self.title isEqualToString:@”Add New Task”])]以下をすべて削除して、このように実装してみました。


	// MARK: *********** 画面遷移処理 ********************************************************** //
	
	BOOL sendFromSubButton = (tag == TAG_SUB_BUTTON);
	
	// "Add Details"ボタンタップ時 or 各編集ViewのSave/Cancelボタンタップ時
	if ((_isFirstEdited && sendFromSubButton) || (!_isFirstEdited && !sendFromSubButton)) {
		
		if (sendFromSubButton) {
			
			DetailViewController *detailViewController = [[DetailViewController alloc] 
														  initWithStyle:UITableViewStyleGrouped 
														  editableObject:_editingTask];
			detailViewController.title = @"Add Detail";
			detailViewController.delegate = _delegate;
			detailViewController.showJudgeButtons = YES;	// Save/Cancelボタンを表示
			
			[self.navigationController pushViewController:detailViewController animated:YES];
			[detailViewController release];
		}
		else {
			[self.navigationController popViewControllerAnimated:YES];
		}
	}
	else {	// "Save & Show List"ボタンタップ時
		
		SaveType saveType = (willSave) ? saved : canceled;
		[_delegate didEditProvisinalTask:_editingTask saveType:saveType];
		
		if (_editingInfo.hasModalViewController)
			[self.navigationController dismissModalViewControllerAnimated:YES];
		else
			[self.navigationController popToRootViewControllerAnimated:YES];
	}

細かいところはいいので、コメント部分を見て何となく分かってもらえれば良いと思います^^;
TodoCoreでの分岐と比べると随分すっきりしたんじゃないでしょうか?

※ハイライトされている15行目の”showJudgeButtons”は、DetailViewControllerでインスタンス変数としていたものをプロパティに変更してあります。
BOOL値なのでプロパティの属性は”(nonatomic, assign)”です。

そしたら、仕上げに“DetailViewController.h”をインポートしときましょう。
 
 
あと、“Add Details”ボタンで遷移されたDetailViewでは、
Save/Cancelされた時に[popViewController:]ではなく
[dismissModalViewController…]する必要があるのでこちらも変更します。

- (void)judgeButtonTapped:(id)sender {
	
	SaveType willSaved = ([sender tag] == TAG_SAVE) ? saved : canceled;
	
	/*! Attention: デリゲートに送る前に判定しないと_editableObjectの状態が変わってしまう */
	BOOL hasModalViewController = [_editableObject isInserted];
	
	[_delegate didEditProvisinalTask:_editableObject saveType:willSaved];
	
	
	if (hasModalViewController)
		[self.navigationController dismissModalViewControllerAnimated:YES];
	else
		[self.navigationController popViewControllerAnimated:YES];
}

 
そしたらビルドする前にTodoの削除も実装しておきましょう。
 
 

3. Todoの削除実装!

RootViewでのスワイプ/Editボタンを使った削除は実装されているので、ここで実装するのは“DetailView”のDeleteボタンのアクションです。

こちらでの処理もまずアラートを表示してから削除するようにしたいので、先に以下の2つのメソッドを追加します。

次に[viewForFooterInSection]でdeleteButtonにアクションを追加すれば完了でっす!

 
[deleteButton addTarget:self action:@selector(deleteButtonTapped) forControlEvents:UIControlEventTouchDown];
 

そしたら、ビルドして色々やって確かめてくださいね〜!
 
 

4. セクションのソートについて

どうでしょう?
実はこの状態ではいくつかの欠陥があるって事に気付いた方がいるかもしれませんね…
 
たとえばTodoを追加/削除したり、カテゴリーを加えたり、カテゴリーを変更したり削除したり、チェックボックスにチェックを入れたり…と色々といじると
エラーが出たり、
エラーが出なくても
実際のカテゴリーとは違うセクションに[task]が表示されてしまったりします。
 

その原因は、まだ実装が不十分な点も含めていくつかあるのですが
原因のひとつがセクションのソートです。

 
まず、今のfetchedResultsControllerの設定を見てみましょう。

ここでは、セクションは@”category.name”でソートの第一条件はKEY_COMP(@”completed”)にしてましたね。

実は、ここにひとつ目の問題があります。

フェッチリクエストに渡すソートの第一条件で得られる集合と[sectionNameKeyPath:]で並べた場合の集合は、同じ固まりにならなくてはいけないようなのです。
(※以下のブログを参考にさせて頂きました<(_ _)>
NSFetchedResultsController : sectionNameKeyPath設定時の注意
– Natsu’s iPhone App
)
 
 
つまりこの場合、すべての[Task]を
“カテゴリー名順に並べた”場合
“Todoが完了したかでどうか?”で並べた場合とでは、
全く違う順番になってしまうのでNG!!ってワケです。
 
 
…では、初めに予定していたように
『カテゴリーに関連しているtasksが多い順』に並べるには、
どうしたら良いんでしょうか?

実はこの条件は何となく決めたものだったのですが、
かなりメンドウです……<(_ _;)>
ですが、勉強の一環として割り切って試してみることにします。
 
 

5. 決してオススメは出来ませんが…

タイトルの通りですが^^; とりあえず実装してみます。

まず、試してみたのはこちら。

 
[NSSortDescriptor sortDescriptorWithKey:@"category.numberOfTasks" ascending:NO],
 

で…結果は見事にエラー…
“エンティティに’category.numberOfTasks’なんてkeyPethは見つかんないぞ!”って…(> , <;)
 
 
この状況を打破するために、
Taskエンティティにソート用の属性を追加することにしました。

まず、モデルエディタで"モデルバージョンを追加"して以下の属性を加えます。

属性名:sectionIndex
オプション/一時:共にOFF
データ型:整数16
デフォルト値:0

 
そしたら、“Task.h”でこの[sectionIndex]を追加します。

で、”Task.m”にも[@dynamic sectionIndex;]しておきました。
 
 
そしたら、この[sectionIndex]に値を入れるのですが
categoryが設定された時に値を入れるようにしてみました。

 
説明しますね。

まず[setCategory:]では、新しい値をセットする前に現在のCategoryを取得しておきます。
で、新しいCategoryをセットした後で
旧Categoryと新Categoryが関連しているすべてのTask
[changeSectionIndexOfTasks:]に送ってます。
 
 
そしてその[changeSectionIndexOfTasks:]では、
すべてのTaskの[sectionIndex]を現在の[numberOfTasks]の値に変更します。

かなり分かりづらい説明でスミマセン…<(_ _;)>
つまり、カテゴリーがセット/変更される度にカテゴリーが関連しているTaskの数は変わりますよね。
それをこの2つのメソッド(+[numberOfTasks])で管理しているワケです。
 
 
これでカテゴリーがセット/変更される度にsectionIndexは更新されるようになりましたが、ココで絶対忘れちゃいけないのがTask削除時の処理!

なぜかと言うと、今の状態では
Taskが削除されてもsectionIndexは更新されないので値が減らない事になってしまいます。
これを防ぐためには削除する前に
[setCategory:]を呼んで更新させる必要があります。

なので”RootVIewController”の2つのメソッドに[setCategory:nil]を加えます。

 
メソッド:[tableView: commitEditingStyle: forRowAtIndexPath:]

		/*! 重要!! 削除前に必ず実行								/
		/	コレをしないと[sectionIndex]が減算されずにエラーになる	*/
		[deleteTask setCategory:nil];
		[managedObjectContext_ deleteObject:deleteTask];
 
 
メソッド:[didEditProvisinalTask: saveType:]

		[_masterTask setCategory:nil];
		context = [_masterTask managedObjectContext];
		[context deleteObject:_masterTask];

 
[追記] 上記の[tableView: commitEditingStyle: forRowAtIndexPath:]のコードが間違っていました<(_ _)>正しくは以下です。

 
メソッド:[tableView: commitEditingStyle: forRowAtIndexPath:]

		NSManagedObjectContext *context = [fetchedResultsController_ managedObjectContext];
		
		Task *deleteTask = [fetchedResultsController_ objectAtIndexPath:indexPath];;
		
		/*! 重要!! 削除前に必ず実行								/
		 /	コレをしないと[sectionIndex]が減算されずにエラーになる	*/
		[deleteTask setCategory:nil];
		
		[context deleteObject:deleteTask];
		
		NSError *error = nil;
        if (![context save:&error]) {...

 
 
 
で、そこまで出来たら”No Category”は一番下に表示させたいので
“No Category”の場合は[tasks]の数に限らず”0″を返すようにしておきます。

 
あとは、ソートでスクリプタの始めの条件に以下を加えます。

 
[NSSortDescriptor sortDescriptorWithKey:KEY_SECTION_INDEX ascending:NO],
[NSSortDescriptor sortDescriptorWithKey:@"category.name" ascending:YES],
 

※@”category.name”はエラー回避のために入れました。
 
 
ここで、今まで触れていなかった重要なポイントがあります!!
 
 
先程、モデルエディタでバージョンを追加して変更しましたが
コレを繰り返しているとアプリが全く起動できなくなることがあるんです。

コレは、モデルエディタでの変更を”PersistentStoreCoordinator”が認識していないために起こるようです。

ですので、AppDelegate.mの
[- (NSPersistentStoreCoordinator *)persistentStoreCoordinator]
を以下のように変更しておく必要があります。

こうして、[addPersistentStore…]にoptionを設定しておく事で、モデルエディタでのバージョン以降が反映されるようになるようです。

詳しくはこちらへ…
Core Data 勉強日記 (8):More iPhone 3 Development / chapter 5 (データモデルのバージョン管理)
– Natsu’s Iphone App

 
 
…とここまでやってビルドしてみました。
 

6. 極めつけのダミー処理

…ちゃんと表示されない…

ですが、よく見てみるとセル自体は意図したようにソートされています。
問題はセクションがおかしな位置に表示されてしまっているってコトです。

原因は恐らくですが、[numberOfRowsInSection:][numberOfSectionsInTableView:]が呼ばれている時点では、
“category”や”category.tasks”がフォールト状態のため
“sectionIndex”でのソートが出来ないからなんじゃないかと思います。

ですが、[configureCell:]ではフォールトが解除された状態になるのでちゃんとソートされるのかな?と。

※エンティティの各プロパティの状態には “フォールト(Fault)”という状態があります。
フォールトとはまだデータがロードされていない状態です。
そして、そのプロパティにアクセスした時点で初めてフォールトが解除されてデータがロードされます。

 
 
なので、コレを強制的に再表示させてあげればちゃんと表示されるはずです。

そのためにまず[- (NSFetchedResultsController *)fetchedResultsController]の以下の部分を削除して別の場所に移します。

    NSError *error = nil;
    if (![fetchedResultsController_ performFetch:&error]) {
        /*
         Replace this implementation with code to handle the error appropriately.
         
         abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
         */
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }

 
 
移動先は[loadView]です。

#pragma mark -
#pragma mark View lifecycle

- (void)loadView {
	[super loadView];

    self.title = @"Todo List";
    self.navigationItem.leftBarButtonItem = self.editButtonItem;
    
    UIBarButtonItem *addButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd 
																			   target:self 
																			   action:@selector(showModalAddTitleView)];
    self.navigationItem.rightBarButtonItem = addButton;
    [addButton release];
	
	
	/*!	*************** ダミー処理 ******************************************************
	/* 初回起動時からソート条件のカテゴリにアクセスできるようにするため、ダミーの新規Taskを作成。 */
	
	Task *dummyTask = [NSEntityDescription insertNewObjectForEntityForName:ETN_TASK 
											   inManagedObjectContext:managedObjectContext_];
	dummyTask.completed = [NSNumber numberWithBool:YES];
	[dummyTask setCategoryWithName:DEFAULT_CATEGORY];
	
	// フェッチ
	NSError *error;
	if (![self.fetchedResultsController performFetch:&error]) {
		// Update to handle the error appropriately.
		NSLog(@"Error performing fetch: %@", [error localizedDescription]);
		NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
		exit(-1);  // Fail
	}
	
	// ダミーでインサートしたtaskを削除
	if (dummyTask) {
		[managedObjectContext_ deleteObject:dummyTask];
		
		if (![managedObjectContext_ save:&error]) {
			NSLog(@"dummy Task Delete & Save : Unresolved error %@, %@", error, [error userInfo]);
			exit(-1);
		}
	}
}

 
完全なるダミー処理です(^〜^;)

要は、テーブルビューのデリゲートメソッドが呼ばれる前
[category]や[sectionIndex]にアクセスしてフォールト状態を抜けちまえ!ってコトです。
 
 
で、念のためこのメソッドと
[- (NSFetchedResultsController *)fetchedResultsController]以外の場所に書かれている
[self.fetchedResultsController]をすべて[fetchedResultsController_]に変えておきます。

※こうすることでloadView以外の場所で、先に
[- (NSFetchedResultsController *)fetchedResultsController]にアクセスする事はなくなります。
[self.fetchedResultsController]はアクセサメソッドを呼びますが、
[fetchedResultsController_]は代入されている値を参照するだけだからです。

 
 
さぁ!これで今度こそ大丈夫なはずです!!
 
 

7. なんとかね…

とりあえずですが、起動時のソートはちゃんと出来るようになりました!
 
 
ですが、前述した通り他にも重大な問題が潜んでいます。
次回はその問題の解決方法と、
それらをふまえたCoreDataを使う際のデータの構造について考えてみたいと思います。
 
 
正直、今回のエントリーは
僕としては一番キツいエントリーでした(^〜^;)

しっかし、次回は僕がこのデモアプリを作る上で最もハマったポイントになります…
 
 
ま、みなさんにも参考になる内容だと思うのでお楽しみに〜

では、また!
 

広告

Study CoreData 18 ~時には強引さも必要?~」への3件のフィードバック

  1. ピンバック: Tweets that mention Study CoreData 18 ~時には強引さも必要?~ « Everything was born from Love -- Topsy.com

  2. ピンバック: [夕刊] Apple, 脱獄は補償の対象外とアナウンス。

  3. ピンバック: [朝刊] アップル新製品!Magic Trackpad, 27インチディスプレイ, iMac刷新! そして12コアの Mac Pro!!!

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中