Study CoreData 6 ~困難はそれを乗り越えられる人に与えられる~

日本の片田舎のEdenVision*スタジオからお送りしている『Study CoreData』。
今回で開局一週間記念となりました。
 
今宵も『Live365』の『Back Tracks Radio』をBGMに、オールディーズなナンバーとともにお送りします♬
 
申し遅れました。
なんちゃってDJのJacminikです!
 

夏と言えば水出し珈琲が美味しい季節ですよね〜
僕は『水出しコーヒーポット』ってのを使って毎日作って飲んでます。

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

  
では、CMいきましょう。
(ここでネスカフェコマーシャル)
 
 
……すみません……
なんか、電波が混線したようです^^;
 
 
では参りましょう。
(さすがに7日も続けて同じ内容のエントリをしてると頭がおかしく…>〜<;)
 
前回は、自動生成されたプロジェクトを元に、セクションを加えたり、ひとつのセルに2つの情報を表示するところまでやりましたね。
 
さらに今回は、<関連>プロパティの追加データの編集用のコントローラを用意するところまでやって行きたいと思います。
 
ってことで、まずは関連の追加ですね。
 
 

1. 関連を追加する(おさらい)

 
今回は構造を単純にして、以下のような関連を加えることにしましょう。

<Event>に関連づけるエンティティを<Action>として作成。
<Action>は<name>という属性を持つ。
<name>は、必須、文字列型で最小長が1、デフォルト値を”No Action”とする。
 
<Event>から<Action>の関連はオプションとし、単一関連、削除ルールはカスケード。
<Action>から<Event>への関連はそのままに。

 
こんな条件でデータモデリングしてみてください。
(テストなのでデータモデルのアップデートはしなくて構いません。)
 

ついでに、前回のようにアプリが強制終了しないように
<Event>の<sectionNumber>属性の最大長を削除(未入力に)しておいてくださいね。
 
 
こんな感じにできましたか?

出来たら次へ行きましょう。

試しに、この<Action>の属性<name>の値を
<timeStamp>の代わりに表示させてみることにしましょう。
 
 

2. 関連にアクセスする(苦悩の日々編)

セルの表示は[configureCell: atIndexPath:]でしたね。
それじゃ、一旦<timeStamp>の値を表示させているところをコメントアウトして、関連<action>に変更してみましょうか?

cell.detailTextLabel.text = [managedObject valueForKey:@"action"];

こんな感じですかね〜?

とりあえずビルドしてみましょう。
(一旦アプリを削除するのをお忘れなく!)
 
ど、どうですか?ちゃんと表示されましたか?

『ちゃんと表示されたよ〜!』って言う人!!
それはバグです!
何かの間違いです。

ってか、これでは間違いなく表示されません。
 
 
あ、これぢゃ値にアクセスしてないから、あれを使えば良いのかな?
って思って

cell.detailTextLabel.text =
 [managedObject valueForKeyPath:@"action.name"];

ってやってもダメです!悪あがきです…
 
 
…なにがいけないんでしょうか…?

とりあえず、<managedObject>の生成部[insertNewObject]をもう一度見てみましょうか。

ここでは<Event>エンティティを作ってましたね。
このエンティティは<属性>と<関連>も持っているはずです。
でもアクセスできない…
(正確にはアクセスしてもnilが返ってくる)
 
 
う〜ん…????

って悩みました。で、気付きました!
気付いちゃいましたっ!!

『<Action>エンティティのオブジェクト作ってないじゃん!!』

『んぢゃ、つくればいいんぢゃね?』

そう、確かにEventエンティティは<Action>エンティティへの関連<action>は持っていますが、そもそも<Event>と<Action>は別々のエンティティ
<Event>は<Action>のことは知ってて話しかけてるんだけど、そもそもその<Action>がそこにはいない….。
独り言かよ!(…恥ずっっ!)
ってことになってた訳ですね^^;
  
 
ってことでどうやってつくればいいのかなぁ…?
ってさらに悩み調べまくってついに答えを見つけました!

で、どうやるかと言うと…

NSManagedObject *actionObject = 
	[NSEntityDescription
		 insertNewObjectForEntityForName:@"Action" 
			 inManagedObjectContext:
			[newManagedObject managedObjectContext]];

ほとんど<Event>エンティティからオブジェクトを作ったのと同じ。
違うのは直接エンティティ名を指定してる点とコンテキストのところですね。

ここでは、<newManagedObject>(Eventエンティティのオブジェクト)からコンテキストを取り出してそれを渡しています。

なるほどなるほど
 
 
で、ポイントは次です!

[newManagedObject setValue:actionObject forKey:@"action"];

ななんと!
そのデータオブジェクトごと 関連<action>の値として渡しちゃえ〜!!

ってやってます。
なんだか、以外にあっさりですね。
 
 
では、やってみましょう!

まずはここにコイツを加えて…

 
…で、アクセスはこんな感じでっと…

(※2010.10.26 追記: リンク先の画像が違っていたので修正しました。)
 
では、行きましょう。
ビルダンゴー!! (ビルドと実行)

きました!きちゃいましたねっ♬ No Title, No Action !!
(ひそかに”No Music, No Life”にしておけばよかったと後悔…)

と、幾多の壁をぶちこわしながら、なんとか関連へのアクセスへ辿り着けたわけです。
 
 
いや〜開発って本当に面白いもんですね〜。
さよならっ。さよなら
(水野晴郎風)

って、まだ終わりませんよ^^;
 
 
このままでは、結局何の役にも立たないアプリなんで、
追加ボタンを押した時にタイトルを入力できるようにしてみたいです。
そのためには、それ用のViewControllerが必要になりますね。

ってことで用意しまっす。
 
 

3. タイトルを入力するためのコントローラを用意する。

分かりやすいように、Classesグループの中に<Other ViewController>というグループを作って、そのなかに<AddTitleViewController>という名前のクラスを用意しましょう。
このビューコントローラは、UITableViewControllerのサブクラスにしておきます。

で、このクラスでタイトルの入力をするためには、<NSManagedObject>のインスタンスをRootViewControllerからもらわなくちゃいけないですよね。
なので、retain属性の<NSManagedObject>のプロパティを用意して、
ついでにテキスト入力用の<UITextField>のインスタンスも用意。

さらに<NSManagedObject>のインスタンスをもらい忘れちゃったら話にならないので、初期化メソッドの引数に入れちゃいましょう。
 
 
ヘッダーファイルはこんな感じにしてみました。

ここで<NSManagedObject>のインスタンス名とプロパティ名が違うのは、この方が何かと便利だからです。
 
 
たとえば、新しく作った初期化メソッドの最後の引数のところにプロパティ名とおなじ<managedObject>が使われていますが、これがインスタンス名と同じだと
『ローカル変数(メソッド内の変数)とインスタンス変数名が同じだぞ!』
って怒られて(警告)インスタンス変数にアクセスできなくなってしまいます

そのために<aManagedObject>とかするのも何だかなぁ〜と思いますし、他にも利点があったりします。
 
 
で、実装ファイルはこうなります。

@synthesizeのところを見てもらうと分かるように、

プロパティ名 = インスタンス変数名

と書くことで、self.managedObject とか [self managedObject] とか _managedObject という風に使えるようになります。

初期化メソッドでは、引数として渡された<managedObject>をそのプロパティで保持するようにしてあります。
 
 

4. AddTitleViewControllerの基本コード

その他の実装についてはCoreDataには直接関係しない部分が多いので、コードをそのままあげておきますね。
 
 
まずは、セルの表示部分から。

#pragma mark -
#pragma mark Table view data source

- (CGFloat)tableView:(UITableView *)tableView 
	heightForHeaderInSection:(NSInteger)section {
	// セクションヘッダの高さを指定してtextFieldのcellを中央に
	return 80.f;
}

- (NSString *)tableView:(UITableView *)tableView 
	titleForHeaderInSection:(NSInteger)section {
	// 何も入れないと[heightForHeaderInSection:]が無効になるので
	// スペースを入れておく
	return @" ";
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
	// Return the number of sections.
	return 1;
}

- (NSInteger)tableView:(UITableView *)tableView 
	numberOfRowsInSection:(NSInteger)section {
	// Return the number of rows in the section.
	return 1;
}


// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView 
	cellForRowAtIndexPath:(NSIndexPath *)indexPath {

	static NSString *CellIdentifier = @"Cell";

	UITableViewCell *cell =
		[tableView dequeueReusableCellWithIdentifier:CellIdentifier];

	if (cell == nil) {
		cell = [[[UITableViewCell alloc] 
		initWithStyle:UITableViewCellStyleValue2 
		reuseIdentifier:CellIdentifier] autorelease];

		textField = [UITextField new];
		textField.frame = 
			CGRectMake(80.f, 10.f, cell.contentView.bounds.size.width - 20.f, 24.f);
		[textField addTarget:self action:nil 
			forControlEvents:UIControlEventAllEditingEvents];
		[cell.contentView addSubview:textField];
		[textField release];
	}
    
	cell.textLabel.text = @"Title:";

	NSString *text = [self.managedObject valueForKey:@"title"];

	// managedObjectから属性を取り出して、そのデフォルト値を取得する
	NSAttributeDescription *attribute = 
		[[[self.managedObject entity] attributesByName] 
		valueForKey:@"title"];

	NSString *defaultTitle = [attribute defaultValue];

	// textがデフォルト値と同じ(まだ編集されていない)なら、
	// プレースフォルダとして表示する。
	if (![text isEqualToString:defaultTitle])
		textField.text = text;
	else
		textField.placeholder = text;

	return cell;
}

簡単に説明します。
 
 
最初のふたつのメソッドで、セクションヘッダを裏技的に使ってひとつしか表示されないセルを真ん中辺りに表示できるようにしてます。
まぁ、とくに気にならない人は書かなくても良いと思います。

そのあとのふたつはそのまま。セクションをひとつ、セルをひとつ表示させます。

で、[cellForRowAtIndexPath:]のところですが、
まずインターフェイスに関してはstyleを<UITableViewCellStyleValue2>にして、左側にTitle:という文字を表示させるようにしています。
そして、本来なら<detailTextLabel>が表示させるところにテキストフィールドを乗っけちゃってます。
(もちろん実際はカスタムセルを用意した方が良いと思いますが、テストなので簡略化してます。)
テキストフィールドのアクションは後で用意するので、今のところはnilに。
 
 
さらにその後ですが、ここはちょっとCoraDataな部分ですね。

要は、デフォルト値であるNo Titleというのを入力されたテキストとは別なものとしてプレースホルダで表示してあげたいわけです。

やってる事は、こんな感じになります。
 
 

5. 属性のデフォルト値を取り出す

まず、managedObjectからエンティティを取り出して、そのattributes(属性全部)を取り出します。
ここで返されるのが<NSDictionary型>なのでそこからキーを指定して属性である<NSAttributeDescription>を取得します。
その<NSAttributeDescription>から<defaultValue>を取り出す。

でも、これはその取り出し部分を書かずに

if (![text isEqualToString:@"No Title"])

とやっても全く同じ結果になります。

なので、こんなコトも出来るよ。ってくらいで憶えておけばいいんぢゃないかと。
ちなみにユーザがNo Titleって入力した場合も同じように処理されちゃいますね…
それを防ぐには別の方法もあったりします。
 
 

6. 分からない事を分かるようにするために。

ちょっと余談になりますが、こういうやり方をどうやって見つけるかっていうと、今までちょくちょくやってみた必殺技(クラス/メソッド名を⌘+ダブルクリック)などで、実装ファイルを辿って行ったり、リファレンスを見てみたりします。
(もちろんとりあえずググったりしますが ^^;)

そうやって、ちょこちょこ調べたり試したりしていくうちに、たま〜にですが、あまり知られていないような『使えるメソッド』に出逢えたりする事もあります。

あと、検索の際に日本語で書かれたものしか見ていない人(僕もちょっと前まではそうでした)には、とりあえず英語の情報も見てみる事を強くオススメします!
書かれたコードは何となく分かったりしますし、分からない英語の部分はとりあえずウェブ翻訳してみる!くらいの感じで見ていくといいかと。

なんて言っても日本語だけの情報より圧倒的に多い知識が手に入りますからね。
 
 
以上。余談でした。
コードに戻ります。
 
 

7. 話を戻して

あとは、初期化メソッドのところで、セルを選択できないように設定して、キャンセル/保存用のボタンを追加しておきます。

- (id)initWithStyle:(UITableViewStyle)style 
	editableObject:(NSManagedObject *)managedObject {

	if (self = [superinitWithStyle:style]) {

		self.managedObject = managedObject;

		[self.tableView setAllowsSelection:NO];

		// ナビゲーションバーにボタンを追加
		UIBarButtonItem *cancelButton = [[UIBarButtonItem alloc] 
			initWithBarButtonSystemItem:
				UIBarButtonSystemItemCancel 
				target:self action:nil];

		self.navigationItem.leftBarButtonItem = [cancelButton autorolease];

		UIBarButtonItem *saveButton = [[UIBarButtonItem alloc] 
			initWithBarButtonSystemItem:
				UIBarButtonSystemItemSave 
				target:self action:nil];

		self.navigationItem.rightBarButtonItem = [saveButton autorolease];
	}
	return self;
}

ここでも、とりあえず各ボタンのアクションはnilにしてます。
そして、ビューが表示された時にすぐ入力できるように

- (void)viewDidAppear:(BOOL)animated {
	[super viewDidAppear:animated];

	[textField becomeFirstResponder];
}

最後に忘れずにプロパティの解放を。

- (void)dealloc {
	self.managedObject = nil;

	[super dealloc];
}

ざっとこんな感じで、肝心のアクション以外の実装は完了です。
 
 
ちょっと中途半端ですが、今日はこの辺で終わりたいと思います。
 
 
♬キ〜ンコ〜ンカ〜ンコ〜ン♬

ですが、宿題がありま〜す(ちょっと先生気取り(笑))
 
 
『え〜〜〜〜〜っ。やだ〜ぁ!!』
とか言わずにお願いします<(_ _;)>

とりあえず<RootViewController>から、今回用意した<AddTitleViewController>を繋いでみてください。
追加ボタンを押した時にモーダルビューとして表示される感じがいいですね。
styleは<UITableViewStyleGrouped>にしておいてください。
(じゃないと表示が崩れます)
 
 
それと<AddTitleViewController>の初期化には、今回用意した初期化メソッドを使うのを忘れないでくださいね。
あと必ず変更前の状態を[スナップショット]に撮っておいてくださいね。
(次回の答え合わせの際に、元に戻せないとめんどくさいですから。)

念のため、スナップショットの使い方はこちら。

メニューバーの<ファイル>ー<スナップショットを作成>か、
<スナップショット>を選択後に出てくる<作成>ボタンをクリックします。

すると、現在のプロジェクトの状態が保存されて、そのあとの変更後でも簡単に変更前の状態に復元できるようになります。
 
 
それではみなさん、また明日、元気な顔でお会いしましょ〜♬

寄り道しないで帰るようにっ!
買い食い禁止!!
 

Study CoreData 6 ~困難はそれを乗り越えられる人に与えられる~」への17件のフィードバック

  1. iPhoneSDK初心者の中の初心者です。自分のアプリがAppStoreに並ぶ日を夢見て猛勉強中です。毎日次の更新が楽しみで仕方ないです!!がんばってください。

    • Junich_さん、はじめまして!
      コメント、本当にありがとうです

      こんなコメントもらえると、本当に始めて良かったなぁと思います♪
      僕もまだ”未リリースの開発者”という点では、一緒です。

      僕も期待に応えられるように頑張るので、
      お互い目標に向かってがんばっていきましょー!!

  2. こんばんは、jacminikさん。
    この、連載すごく楽しみにしています。
    僕も、まだ未リリースなので、このサイトを見ているとすごくがんばろう!と思います。
    特に、エンティティーとリレーションシップのモデリングの部分に興味があります。

    • bane1983さん。
      こちらも、ほんっとにウレシイコメント本当にありがとうございます。

      モデリングについては、デモ動画と同じアプリを作るところでより詳しく説明することになると思います。
      ですが、その後のアプリを作っていく途中でも色々と『属性を追加』したり、不具合が出たり…^^;

      なにはともあれ、最後まで一緒に頑張って頂けると見えてくるものもあると思うので。
      今後ともよろしくです

  3. ピンバック: [朝刊] iOS 4.0.1 、シグナル表示の変更版出た。iPad用 3.2.1 Wifi 接続改善版も同時に公開。

  4. こんにちは。
    ちょくちょく拝見し、勉強させていただいてます。

    ビルドした結果のiPhone画面画像のひとつ上にあるソースの画像、
    クリックすると違うものになっているようです。
    一応ご報告までに。

    これからも頑張って下さい。
    楽しみにしております。

    • 応援&ご指摘ありがとうございます<(_ _)>

      早速修正させてもらいました。
      また何か気付いたことがあれば遠慮なくコメントもらえるとありがたいです^^;

  5. はじめまして!
    CoreData参考にさせていただいています。
    詳しい記事なのでとても勉強になります。
    Study CoreData 5 ~まだまだ序盤~ までは無事できました。
    2. 関連にアクセスする(苦悩の日々編)でつまづきました。
    blogを参考にしながらデータモデリングも行い、insertNewObject に NSManagedObject *actionObject 関係も追加し、[newManagedObject setValue:actionObject forKey:@”action”];も追加しました。

    cell.detailTextLabel.text = [managedObject valueForKey:@”action.name”];も追加しました。
    よしこれでOKと、ビルダンゴー!! (ビルドと実行) を行いましたが、追加(+)をクリックするとエラーになりました。
    エラーの内容は、
    Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. [ valueForUndefinedKey:]: the entity Event is not key value coding-compliant for the key “action.name”. with userInfo {
    NSTargetObjectUserInfoKey = ” (entity: Event; id: 0x6100910 ; data: {\n action = \”0x6107950 \”;\n sectionNumber = 0;\n timeStamp = \”2011-02-25 06:08:11 +0000\”;\n title = \”No Title\”;\n})”;
    NSUnknownUserInfoKey = “action.name”;
    }

    データモデルには、エンティティ Action に 属性 name も作成されています。でも、UnknownUserInfoKey となります。
    原因として何が考えられますでしょうか?

    データモデルと追加分を私のblogにアップしてみました。
    間違いをご指摘いただけないでしょうか・・・
    http://ameblo.jp/iphone0126/

    • cowさん、はじめまして!
      参考にして頂いているようでありがとうございます

      頂いたコメントの件ですが、しばしお待ちくださいね。
      (週末はちょいと忙しいので。。^^;)

    • 返信遅くなってしまってごめんなさいです。。<(_ _;)>

      とりあえずですが、訂正ポイントがみつかったのでもう解決済みかも知れませんが、以下に書いておきますね。

      誤)cell.detailTextLabel.text = [managedObject valueForKey:@”action.name”];
      正)cell.detailTextLabel.text = [managedObject valueForKeyPath:@”action.name”];

      つまり、指定するキーに”.”が含まれている場合はvalueForKeyではなくvalueForKeyPathになります。
      だからエラーに”NSUnknownUserInfoKey = “action.name””と出ていたようですね。
      (単純なミスですが僕もよくやってました^^;)

      これで動けばいいのですが。

  6. ”[newManagedObject setValue:actionObject forKey:@”action”];”と”cell.detailTextLabel.text = [managedObject valueForKeyPath:@”action.name”];”の”action”は”actions”ではないですか?
    こちらだと通ります。

    しかし、なぜかセクション1以降は1行ごとにセクションが切り替わってしまいます。Actionエンティティを入れる前はこのエラーはありませんでした。原因はまだわかりません><

    ちなみに、xcode4を使用していてデータモデル選択の仕方がわからず、前に作成したTask、Tag、Categoryエンティティも同じxcdatamodeldに入れています。今のところ問題なく動いています。

    • @yonestraさん、コメントありがとうございます。

      ご指摘の件ですが、こちらも本エントリの通りにやっていけば”actions”ではなく”action”で通ります。
      (※1. 関連を追加する(おさらい)の記述と画像を参照)
      僕は基本的に対多関連でない限り複数形の属性名は使わないようにしています。

      xcdatamodeldも基本的にはアプリにひとつなので問題ないと思います。

      セクションが一行ごとに増えていってしまうのは恐らくですが、ひとつ前のエントリの
      『4. 最後のメソッドを解読する』
      で書いている[newSectionNumber]メソッドかその前後辺りに原因があるかと。。

  7. いつもブログ拝見して勉強させてもらっています。

    – (id)initWithStyle:(UITableViewStyle)style editableObject:(NSManagedObject *)managedObject
    {
    if(self = [super initWithStyle:style]){
    self.managedObject = managedObject
    }
    return self;
    }
    の部分で質問なのですが、

    self.managedObject = managedObject
    ↑の処理は必ず必要な処理になってきますか?
    今、UIViewControllerを継承したクラスで追加先を作っているのですが、
    UIViewControllerのほうでは、このメソッド通らないですよね?
    もし、代替え案など知っていましたら、教えていただけると嬉しいです。

    • pondemさん、はじめまして!

      質問の件の [self.managedObject = managedObject;] ですが、
      UIViewControllerのサブクラスでmanagedObjectを利用する仕様であれば必要かと思います。

      で、pondemさんの作成したクラスで上記箇所が通らない理由は、managedObjectプロパティを実装していないからではないかと。
      @property と @synthesize さえしてあればココがエラーになることは無いので。

      。。と、そんな感じですがご参考まで。

  8.  
     このサイトを知ってとても助かりました。ありがとうございます!
     CoreDataをあきらめかけていたところでこのサイトを知りました。
     なかには厳しい意見もあるようですが、本当に助かります。ありがとう!
     これからも頑張って下さい!
     (2年前の投稿にコメントするのもどうかと思いましたが、気持ちを伝えたくて書かせていただきました。)

    • コメントありがとうございます!
      そういって頂けるととても嬉しいです。

      ただ、古い記事なので現状とは違う部分もありますので、お気をつけ下さいませ

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