Study CoreData 16 ~考えて実行。考えたら実行!~

みなさん、こにゃにゃちわ!

昨日も触れましたが、今日は『龍馬伝』の日ですね〜♬
いやはや、楽しみです!!

それとは全く関係ないですが
僕は先日、自宅の畑(とは言っても何も植えてませんが^^;)のボーボーになった雑草を2時間ほど掛けてすべて刈りました。

午前中とは言え、夏の暑い陽射しの中での作業中

『オレが成功したら、こんな畑なんて…』

…と弥太郎(地下浪人から成り上がった三菱財閥の創業者)のような気になったりもしました。

 
 
ま、前置きはこれくらいにして
明日の成功を胸に描きながら今日も始めるとしましょうか。


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

 
 

1. 編集結果を保存するタイミング

昨日のエントリの最後で大まかな道すじを決めましたので、
今日からはそれに沿って進めていきたいと思います。

で、昨日はDetailView内での編集は出来ましたね。
なので今日はまず、[EditableViewController]での編集(titleとdueData)を仕上げていきたいと思います。

では先に[title]の編集から行きましょう!
 
 
titleの編集については『新規Todoの保存』は出来る状態になっているので
編集画面の場合のSave/Cancel後の画面遷移を加えればOKですね。

編集画面では値を入れるだけにして保存はDetailViewで行います。

その理由はこうです。

RootViewでセルが選択される。

provisinalContextを作ってDetailViewへ

DetaiViewからEditableViewへ行き編集

ここでデリゲートメソッドへ送ってしまうと…

2つのコンテキストを合成して、provisionalContextを削除。

EditableViewからDetailViewへPop

DetailViewで表示していたTaskのコンテクスト(provisionalContext)がない

エラー!!

ちょっとこの説明だけだと分かりにくいかもですが、
要するに、デリゲートメソッドに送るタイミングはRootViewに戻る直前でなければいけないってコトです。

なので、ここでは保存をしてはいけないんですね。

これで、タイトルの編集はOKなので[dueDate]の編集処理にいきましょう。
 
 
こちらは簡単です。
まず、datePickerのアクションを用意して
そこで、テキストの色を黒に変更(グレーで表示されている場合)。
で、textFieldに選択された日時を表示します。

そしたら、そのメソッドをdetaPickerのアクションに割り当てといてくださいね。

で、最後にDetailViewController.mの[viewWillApper]で
“JudgeButtons”をアニメーション無しで表示させてデータをリロード。

 
 
そしたらビルドして確認してみてくださいね。
コレでタイトルdueDateの編集と保存/キャンセル処理が出来てればバッチリです!
 

2. SelectableViewに必要なもの

次に手を入れるのはもちろん[SelectableView]ですね!
ただ、EditableViewとは違いこちらはまだあまり手を付けていませんでしたし
リレーションシップをいじるコトになるので、ちっくと慎重にいった方が良さそうですね。

ってコトで画面を見ながらちょいと考えてみましょう。

SelectableViewに必要な要素

・リストを表示するために同じ種類のエンティティを全て取得する。

・ツールバにテキストフィールドとボタンを設置

・テキストを入力している時はリストを編集できないようした方が良い?

・テキスト入力中はAddをSaveに、EditをCancelに変える。

・Categoryではセルが選択された時に、それまで選択されていたセルのチェックを外す。

ん〜。
ちょくちょくと細かい設定が必要になりそうですね。

で、まずはヘッダはこんな感じにしてみました。

#import <UIKit/UIKit.h>
#import "EditValueController.h"


@interface SelectableViewController : EditValueController {

	NSArray *_listObjects;					// TableViewのデータベースとなる配列
											// (Categorys OR Tasks)
	
	UIToolbar *_toolbar;					// 新規オブジェクト作成用
	UIButton *_newObjectSaveButton;			// 新規オブジェクトの保存用 
	UIBarButtonItem *_currentRightBarItem;	// 現在のRightBarItem
	
	UITableViewCell *_currentCheckedCell;	// 現在チェックされているセル
}

@property (nonatomic, retain) NSArray *listObjects;
@property (nonatomic, retain) UIBarButtonItem *currentRightBarItem;

@end

 
そしたら、実装ファイルの方で[listObject]と[currentRightBarItem]をsythesizeして、[dealloc]に以下を加えておいてください。

self.listObjects = nil;
self.currentRightBarItem = nil;

 
 

3. データソースを取得してソート。

お次ぎはデーターソース(listObjects)の取得について考えてみましょう。
ですが、fetchedResultsControllerがない状態でデータの取得はこれがはじめてですよね。
なのでそこは後回しにするとして、先に
データを取りだした後のソートについて決めておきます。

今回は、[Category]の場合も[Tag]の場合もそのオブジェクトを関連に持っている[Task]の数が多いもの順に並べたいと思います。
で、数が同じなら名前でソートします。
 
 
でも、そう言えばソートデスクリプターには”Key”を渡すんでしたよね。
考えてみたら、関連しているTaskの数を返す”Key”なんて用意してません(> 0 <;)

…そんなワケでそいつを用意します!

[Category.h]を開いて、まずはこんな感じで他の属性と同じようにプロパティを宣言します。
(※ここでは”assign”にしてますが”readOnly”も加えた方が良いかもしれません。)
 
そしたら[Category.m]で@dynamicしてtasks(NSSet)の数をカウントして返せばOKです。

そして、[Tag]の方にも全く同じものを追加して
[Task.h]で、今用意したKeyを#defineしときます。
#define KEY_TASKS_COUNT @”numberOfTasks”
 
 
これで、Tasksの数は返せるようになったのでデータソースの取得にいきます。

『TodoCore』のはじめの方で[fechedResultController]のゲッタメソッドを解読した時の事をちょっと思い出してみてください。
そのメソッドの中でフェッチリクエストを作って、エンティティやソートデスクリプターをセットしたりしてましたね。
そして最後に[performFetch:]することでデータをfetchedResultsControllerにセットしていました。

実はCoreDataには他にもデータをフェッチ(取得)する方法があるんです!

最後の部分を見てもらうと分かるように、
コンテクストの[excuteFetchResult: error:]というメソッドで
指定したエンティをありったけ取得できます。
簡単ですね♬

それじゃぁ、この[- (NSArray *)allRelationshipsForEntityName:(NSString *)entityName]をTaskに加えてみてくださいね。
(ヘッダファイルでの宣言もお忘れなく!)
 
 
そうしたら今加えたメソッドを使って、[SelectableView]のデーターソースをセットしましょう!

ここでも最後の行を見てもらうと
NSArrayに[sortedArrayUsingDescriptors:]とする事で、ソートされた配列を得ることができます。
こちらも簡単ですね〜。
 
 

4. 表示関連の実装

では次に、さっきの[setDataSource]を呼び出すために[LoadView]をオーバーライドしちまいます。

#pragma mark -
#pragma mark View lifecycle

- (void)loadView {
    [super loadView];
	
    self.navigationItem.rightBarButtonItem = self.editButtonItem;	
	
	CGFloat width = self.tableView.bounds.size.width;
	
	// 新規オブジェクト作成用のツールバー
	_toolbar = [UIToolbar new];
	_toolbar.frame = CGRectMake(0.f, -1.f, width, 40.f);
	[_toolbar setBarStyle:UIBarStyleDefault];
	self.tableView.tableHeaderView = _toolbar;
	[_toolbar release];
	
	_textField = [UITextField new];
	_textField.frame = CGRectMake(5.f, 5.f, width - 65.f, 30.f);
	_textField.delegate = self;
	_textField.borderStyle = UITextBorderStyleRoundedRect;
	_textField.clearButtonMode = UITextFieldViewModeWhileEditing;
	[_textField setContentVerticalAlignment:UIControlContentVerticalAlignmentCenter];
	_textField.returnKeyType = UIReturnKeyDone;
	
	[_toolbar addSubview:_textField];
	
	// 新規オブジェクト追加ボタン
	UIButton *addButton = [UIButton buttonWithType:100];
	addButton.frame = CGRectMake(width - 55.f, 5.f, 50.f, 30.f);
	[addButton setTitle:@"Add" forState:UIControlStateNormal];
	[addButton addTarget:self action:nil forControlEvents:UIControlEventTouchDown];
	[_toolbar addSubview:addButton];
	
	// 切り替え用ボタン
	_newObjectSaveButton = [UIButton buttonWithType:102];
	_newObjectSaveButton.frame = addButton.frame;
	[_newObjectSaveButton setTitle:@"Done" forState:UIControlStateNormal];
	[_newObjectSaveButton addTarget:self action:nil 
				   forControlEvents:UIControlEventTouchDown];
	[_toolbar addSubview:_newObjectSaveButton];
	_newObjectSaveButton.hidden = YES;	// 非表示にしておく。
	
	
	[self setDataSource];
}

ここも特に難しいところはないと思いますが、やっている事はこんな感じです。

1. テーブルヘッダにツールバーを配置。
2. (1)の上にテキストフィールドと追加ボタンを設置。
3. (2)と同じ位置に”追加処理を完了するボタン”を設置して非表示にしておく。

つまり、
新規オブジェクト(Category or Tag)を作るために(2)の”追加ボタン”を押すと(3)の”Doneボタン”が表示されて、”Doneボタン”を押すと追加完了!
ってなることにしました。
(各ボタンのアクションは後で設定します。)
 
 
で、[viewWillAppear]もオーバーライドします。

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
	
	_textField.placeholder = (_editType == EditTypeTag) ? @"Add New Tag" : @"Add New Category";
	
	[self.tableView reloadData];
}

ここでは、プレースフォルダとなるテキストを設定してテーブルビューをリロード。
説明は不要ですね(^〜^)
 
 
それでは、セルの表示を完成させましょう。
ってコトで[numberOfSectionsInTableView:]を削除して、
[numberOfRowsInSection][configureCell: indexPath:]をオーバーライっ!

まず、セクションの行数は[_listObjects]の数を返せばOKですね。

で、[configureCell:]の方では listObjectsから
データオブジェクトを取り出して[name]と[numberOfTasks]を表示
そして、そのデータオブジェクトが編集中のTaskと関連しているならチェックマークを表示してそのセルを変数に入れておきます。
(※[_currentCheckedCell]はCategoryの選択解除時用にしか使わないので、Tagの場合の事は考えなくても大丈夫です。)
 
 
上の画像のハイライト部分で[containsObject:]ってのを使っていますが、
これはNSSetNSArrayのメソッドで
[tags containsObject:aObject]
のように使います。

つまり“aObject”が配列に含まれていれば”YES”を返すメソッドなんですが、
このままだと対象が”Category”の場合は、[containsObject:]がないのでエラーになってしまいますよね。

なので、Categoryにもこのメソッドを実装させることにしましょう。

“category”と”task”の関連は“一対多関連”(One to Many Relationships)でしたね。
つまり[tags]とは違ってtaskが持てる[category]は常にひとつだけなので
セルに表示するオブジェクトとtaskが持つ[category]が同じ時だけ”YES”を返せば良い。
 
 
簡単ですね。

- (BOOL)containsObject:(id)anObject {
	return (self == anObject);
}

これをCategoryに追加しておくだけです。
(ヘッダファイルでのメソッドの宣言も忘れずに。)
※ちょっと省略してますが”(self == anObject) ? YES : NO;”とするのと同じです。
 
 
よしっ!
これでセルの表示は完璧ですっ!!(たぶん^^;)
 

5. セル選択時の動作

ここでもまた次に実装する『セル選択時の処理』について考えてみましょう。

・チェックされていないセルがタップされた場合…
 [A] Categoryリストの場合
   1. タップ前にチェックされていたセルのマークをなくす。
   2. タップされたセルにチェックマークをつける。
   3. 選択されたcategoryを[_editingTask]にセット。
   4. 2つのセルの”タスク数”を更新。
   5. ViewをPop。
 [B] Tagリストの場合
   1. タップされたセルにチェックマークをつける。
   2. 選択されたTagを[_editingTask]に追加。
   3. セルの”タスク数”を更新。

・チェックされているセルがタップされた場合…
 [A] Categoryリストの場合
   何もしない。
 [B] Tagリストの場合
   1. チェックを外す。
   2. 選択されたTagを[_editingTask]からリムーブ
   3. セルの”タスク数”を更新。

こんな感じでしょうか?
 
では、みなさんも[didSelectRowAtIndexPath:]の実装を書いてみてくださいね。
 
 
できましたか〜〜?

それじゃぁ、見比べてみてくださいね。

内容は上でリストアップしたのと同じですね。
ですが、もしかしたらこの中で[self setDataSource][tableView reloadData]
しちゃった方もいるのではないでしょうか?

今回なぜそれらをしていないかと言うと…

1. [reloadData]をするとその後に[configureCell:]が呼ばれるため、accessoryTypeの変更がそこでもおこなわれてしまう。

2. [setDataSource]するとセル表示順が変わる可能性がある。

この2つを避けるためです。
 
 
[2]については実際にビルドしてみると分かるのですが、
タップされて“numberOfTasks”の値が変わって急に表示の順番が入れ替わってしまうとユーザーが混乱する恐れがあります。
なので、今回の実装では並び替えはせずに“タスクの数”だけを書き換える仕様にしてみました。
 
 

6. ここらで一服

[SelectableView]は実装する部分が多いので、今日は駆け足で説明してきましたがいかがだったでしょうか?

これでも“データオブジェクトの追加”“ボタンの切り替え”など、
まだまだ実装すべきコードは残っているんですよね…\(> 〜 <;)/
 
 
ですが、ちょいと長くなってしまったので
ここらで一息ついてちょっくらビルドしてみましょう!
 
 
どうでしょう?
まぁ、新規オブジェクトの追加が出来てないので確認できないところも多いですが、もしかしたらこんな風に表示されちゃった方もいるんじゃないでしょうか?

これには実は2つの問題があります。

1. 同じ名前の異なるCategory(エンティティ)が複数作られてしまっている。
2. “No Category”はユーザーに認識させるべきではない。

[1]は見たまんまですね^^;

で[2]ですが、”No Category”というカテゴリは本来
『カテゴリが設定されていないTodoですよ〜』
ってコトを示すためのものなので、
RootViewでセクション分けするとき以外はユーザーに明示するべきではない
と思います。

なので、次回はこの点の修正もしていきたいと思います。
 
 
梅雨もすっかりあけて暑い日が続いてますが
みなさま、くれぐれも体はお大事に〜!
 
 
では、また明日っ(^〜^)/゜

Study CoreData 16 ~考えて実行。考えたら実行!~」への5件のフィードバック

  1. ピンバック: Tweets that mention Study CoreData 16 ~考えて実行。考えたら実行!~ « Everything was born from Love -- Topsy.com

  2. ピンバック: [朝刊] The Tower for iPad のスレッドに開発者様がきてる!早くリリースを!!!

  3. ピンバック: 頭と尻尾はくれてやる!

  4. ハマってたところがようやく解決できました!
    いい情報をありがとうございました!

    • こちらこそコメントありがとうです
      もう古い記事なので、今ではそのまま利用できない内容などもあるかとは思いますが、お役に立てたなら嬉しいです!

jacminik への返信 コメントをキャンセル