Study CoreData 11 ~急がば回れ!~

性懲りもなく本日も更新していく『Study CoreData』。

第0回から数えると今回で10日を超えたわけですが、
実は実際にここまで辿り着くにはほぼ一ヶ月以上かかっていたりします^^;

一週間以上同じトコロでハマって抜け出せなくなりそ〜な事もありました。
 
 
そんな経緯もあり、ちょこちょこと
ハマりやすいポイントなんかも含めて書いている感じです。

なので、正解だけが書かれた他の書籍などに比べると
随分と遠回りな旅かもしれませんが、じっくりCoreDataを身につけられたらなぁと思って今日も書いていきますので、どうぞよろしゅうに<(_ _)>


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

  
では、今日はまず
例の[valueToCategory]の実装から行きましょう!
 

1. NSDateFormatterとNSString

やる事は基本的に『TodoCore』でやった事と同じなのですが、
違う点が2つあります。

ひとつめは日付表示の部分で、今回は
“7月20日(火) 18:20”
こんな感じにしたいと思います。

ふたつめは[tags]NSSetの文字列化です。
これはこれまでに出てきませんでしたね。
なので、これも加える必要があります。
 
 
で、こんな感じにしてみました。(DTDValueCategorys.m)

ますはNSDateのカテゴリの方から説明しますね。

始めのif文にある[distantFuture]ってのは
NSDateのめっちゃくっちゃ遠い未来(最大値?)を返します。
でSelfがそれと等しければ“No Date”という文字列を返させてます。

なんでこんなまどろっこしい事をしているかと言うと、
[dueDate]に日付が入れられているものを先に並ばせたいからです。

前回のソートデスクリプタに入れていた中に
“古い日付のものを先に”という条件がありましたが、
dueDateがnil(0)だった場合、日付があるものより先に並んでしまいます。
なので、
日付が入れられていない場合、[dueDate]は最大値を返す。
ようにして、それを利用しようってワケです。
※もちろん後で[Task]クラスのdueDateにも手を加える必要があります。

で、[dueDate]に値が入っていれば
ユーザのロケールを取得して前述した表記で表示させます。

ここでですが、なぜか日本語表記や中国表記に必要な”日”が省略されて
“7月20(火) 18:20”みたいになってしまうので、それを修正してみました。

途中の[localeIdentifier]
日本なら “ja_JP”
アメリカなら “en_US”
といった文字列を返すので、その始めの文字列を取得するために[hasPrefix]を使っています。
で、一応 日本語/中国語、韓国語、その他で分岐して
表示を変えられるようにしてみました。
 
 
そして[NSSet]の方は
NSSetの中にオブジェクトがあれば、その値[name]を取り出して
“、”で区切ってそれぞれの文字列をまとめてます。

ココで使っている
[stringByAppendingFormat:]
というのが文字列を連結して、ひとつのNSStringのオブジェクトにしてくれます。
 
あとはヘッダファイルの方にもメソッドの宣言を入れておいてくださいね。
 
 
そこまで出来たら、忘れる前に[dueDate]をいじっておきましょう。

2. アクセサメソッド改造!

ちょっとタイトルが大げさですが^^;
今はTask.mで[dueDate]のゲッタメソッドをカスタムするだけです。

注意する点としては、NSManagedObjectのサブクラス
自分のプロパティにアクセスする時には
[setValue: forKey:] や [valueForKey]などは使ってはいけない
らしいのです。

なので、上のように
[willAccessValueForKey:]で『アクセスするよ〜』って言ってから
[primitiveValueForKey:]で値を取り出して、
[didAccessValueForKey:]で『アクセス完了致しましたっ!!』
ってやってます。

こういった場合に使うメソッドには他にも
[willChangeValueForKey:] や [setPrimitiveValue:]などがあります。

あとは、上で説明したようにnilならdistantFutureを返せばOK!
それとヘッダファイルの方で[DTDValueCategorys.h]をインポートしておいてください。
 
 
これで[Task]をインポートするだけで[valueToString]が使えるようになりました!
そこまで出来たら早速RootViewで表示されるカスタムCellの表示関連に手を付けるとしましょう。
 
 

3. 手を出す前に考える

デモ動画でも紹介しましたが、dotodoで使用するカスタムセルはこんな感じでしたね。

いきなりコーディングに入る前にまず、このセルを表示するために必要な要素を考えてみます。

1. 表示に必要なデータ(プロパティ)
・title
・dueDate
・priority
・completed

2. 必要な操作に対するレスポンス
・タップによってチェックボックスの画像を変える。
・titleとdueDateをグレーに変える。
・completedの値を変える。

こんなところでしょうか?
 
 
1.に関して簡単に言うと、Taskエンティティの[属性]のcreated以外すべてですね。
なので、カスタムセルにTask(NSManagedObject)をまるごと渡してしまって、セル内ですべてを処理しても良さそうに思えるんですが、
これではRootViewControllerでの一元管理が出来なくなってしまう可能性があります。

そして何より、そうしてしまうと
TaskオブジェクトをRootViewControllerとカスタムセルの両方で所有する
ようになってしまう気がします。

それだと何かとマズい事がありそうなので、セルにはその値だけを渡すことにしたいと思います。
 
 
次に2.についてですが、前述したようにセルに値だけを渡すとなると
セル内では値の変更はできないことになるので、
これをRootView側で処理させるためのデリゲートが必要そうだなって事が分かります。

あとはテキストの色を変えるためのフラグと
セル内でタッチイベントを取得することが必要ですね。
(CoreDataには直接関係ありませんが、セル内でのタップ判定についてご存じない方もいるかと思いますので、その辺りも説明しながら進めたいと思います。)
 
そこまで分かったら[ListViewCell]のコーディングに参りましょう!
 
 

4. カスタムセルでタッチイベントを取得

みなさんご存知のようにUITableViewCellは、CPUを結構使うヤツなので
本来ならUIView系のオブジェクトを出来るだけ少なくして、CoreGraphicsを使った描画にした方がスクロールなども格段にスムーズになります。

ですが、今回はCoreDataがメインなのでAPIで用意されているスタイル[UITableViewCellStyleSubtitle]をカスタムして使用することにします。

この[Subtitleスタイル]はTodoCoreでも使用しましたが、
[cell.imageView]を使えば 左側に画像が表示されるのでそいつが利用できますね。
 
 
では、まず[ListViewCell.h]から。
(CoreDataに直接関係しない部分が多いので、そのままコードを乗せます。)

#import <UIKit/UIKit.h>

@protocol ListViewCellDelegate;

@interface ListViewCell : UITableViewCell {

	id <ListViewCellDelegate> _delegate;
	
	BOOL _completed;
	NSUInteger _priority;
	UIImageView *_priorityView;
}

@property (nonatomic, assign) id <ListViewCellDelegate> delegate;

@property (nonatomic, assign) BOOL completed;
@property (nonatomic, assign) NSUInteger priority;

@end

@protocol ListViewCellDelegate

- (void)updateValueWithCell:(ListViewCell *)updateCell;

@end1

 
まず、delegateメソッドには
『どのセルが値をアップデートしたのか?』
が分かるようにセル自身を引数に入れることにしました。

その他に用意したのは、
RootViewから[completed][priority]をセット/取得出来るようにするための
それぞれのプロパティと[priority]の画像を表示するためのUIImageViewのインスタンスの宣言です。

まぁ、そんなに難しいところはないと思います。
 
 
で、お次ぎは[ListViewCell.m]ですね。

まず、プロパティ宣言したもの(3つ)を[synthesize]しといてくださいね。

前述した通り、このセルには各Taskの値だけがRootViewから渡されます。
つまり、2つのtextLabelにはテキスト
completedとpriorityには数値が送られるわけですね。

なので、画像の表示切り替えはこのセル側で処理する必要があります。

で、その処理をどこでするかと言うと
当然、数値がセットされた時ですよね。

って事で、表示を切り替えるためにセッタをカスタムします。

- (void)setCompleted:(BOOL)flag {

	if (_completed != flag) {
		_completed = flag;
		
		[self layoutSubviews];
	}
}

- (void)setPriority:(NSUInteger)value {

	if (_priority != value) {
		_priority = value;
		
		[self layoutSubviews];
	}
}

このように各値が変更された時だけ[layoutSubviews]を呼んで、そっちですべての表示切り替えを行います。
 
[layoutSubviews]でやる事は…

・completedの画像を変更。
・priorityの画像を変更。
・[_priorityView]が無ければ作る。
・completedの状態によってテキストの色を変更。

 
ってコトで、こんな感じにしてみました。

- (void)layoutSubviews {
	[super layoutSubviews];
	
	self.imageView.image = [UIImage imageNamed:
							[NSString stringWithFormat:
							 @"Checkbox_%d.png", _completed]];
	
	if (_completed) {
		self.textLabel.textColor = 
		self.detailTextLabel.textColor = [UIColor lightGrayColor];
	}
	else {
		self.textLabel.textColor = [UIColor blackColor];
		
		if ([self.detailTextLabel.text isEqualToString:@"◐ No Date"])
			self.detailTextLabel.textColor = [UIColor lightGrayColor];
		else 
			self.detailTextLabel.textColor = 
			[UIColor colorWithRed:0.f green:0.47f blue:0.57f alpha:1.f];
	}
	
	
	if (!_priorityView) {
		_priorityView = [UIImageView new];
		_priorityView.frame = 
		CGRectMake(self.contentView.bounds.size.width - 10.f, 32.f, 10.f, 10.f);
		[self.contentView addSubview:_priorityView];
		[_priorityView release];
	}
	_priorityView.image = [UIImage imageNamed:
						   [NSString stringWithFormat:@"Priority_%d.png", _priority]];
}

ここも特に難しいところはないと思いますが、ひとつだけ。
[detailTextLabel]で比較している“◐ No Date”の部分ですが、
このマークには特に意味はないです^^;
(“時計アイコンの代わり”くらいに思ってもらえると助かります)

ここまで終わったら
[initWithStyle: reuseIdentifier:][setSelected: animated:]
いじらないので削除しておいてください。
あと[dealloc]に

self.delegate = nil;

を加えておきましょう。
 
 
で、タップ処理にうつるんですがむずかしそぉですね〜
 
 
ですが…

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
	
	// タップされた位置を取得
	CGPoint locationInView = [[touches anyObject] locationInView:self];
	
	// imageViewより左なら…
	if (locationInView.x <= self.imageView.bounds.size.width) {
		
		self.completed = !_completed;			// completedを変更
		
		[self.delegate updateValueWithCell:self];	// RootViewへ通知
	}
	else {		// imageViewより右なら通常操作
		[self.nextResponder touchesBegan:touches withEvent:event];
	}
}

これだけです!

以外にあっさりでしょ!?

要はタッチイベントを受け付けるためのメソッドを実装するだけです。
セルのスワイプなど、より細かい判定をする場合は[touchesMoved]なども実装する必要があります。

本来は
『imageViewがタップされたかどうか?』
を判定するところだと思いますが、そこまで厳密にするよりもx値のみで判定する方が反応も良くなると思って、上のような感じにしてみました。

それと、imageView以外の場所がタップされた時の処理として
[nextResponder]へイベントを送る事で、通常の『セルの選択』『スワイプによるEdit』にも対応することができるようになります。

ここまででカスタムセルの実装は完了です!
あとはRootView側のセルの表示を変更しておきましょう。
 
 

5. セルの構成再び

セル本体に手を付ける前に、まずRootViewControllerをcellのデリゲートに対応させなくちゃですね。

これは前にもやったのとおんなじ感じで。
(RooViewController.h)

画像のハイライトされたところを追加すればオッケ。
 
 
そしたら、[RootViewController.m]のセルの構成を変更しましょう。
先に説明するので復習のつもりでやってみてください。

・[valueToString]を使用するために“DTDValueCategorys.h”をインポート。
[cellForRowAtIndexPath:]内のUITableViewCellを“ListViewCell”に変更。
・[configureCell:]の第一引数のクラスを”ListViewCell”に変更。(宣言部は変更しない。)
[configureCell:]の処理。
 1. [fetchedResultsController_]から”Task”オブジェクト(*task)を取得。
 2. cellのデリゲートをselfに。
 3. textLavelに[title]を入れる。
 4. detailTextLavelに◐マークをつけた[dueDate] を入れる。
 5. [completed]と[priority]に値を入れる

 
 
出来たら画像を見てみてください。

※分かりやすいように[configureCell:]の位置を変えてあります。

どうでしょう?
おんなじ感じに出来ましたか?

あとは[updateValueWithCell:]の実装ですね。
 

5. セルのタップを記憶する。

セルがタップされた後で[completed]の値を変更するために必要なのは…

1. updateCellの[task]を取得。
2. そのためにtavleViewからupdateCellの[indexPath]を取得。
3. 取得した[task]のcompletedの値を変更。
4. [task]を保存。

こんな感じかと。

きっとココまで一緒に勉強してきたみなさんなら朝飯前ですよね♬
 
 
…で、こんな感じになりました。

 
これで、やっとセルの表示は完成しました!

おつかれさまでした〜!
 
 
ってとこで今回はお開きに。
 
 
今回はCoreDataに関する部分はちょっと少なかったかもですね。
で、次回は編集 + 新規作成用ビューの実装に入っていきたいと思ってます。

ちょっとまだゴールは見えて来ない感じですが
確実に前に進んでるはずです!!

それじゃぁ、また明日!

はりきっていきましょ〜♪

広告

Study CoreData 11 ~急がば回れ!~」への4件のフィードバック

  1. ピンバック: [夕刊] 米アマゾン、電子書籍の販売数が書籍抜くとかすげー。

  2. こんばんは。
    第一弾を何度も読み直してある程度理解出来たので第二弾の方に1ヶ月以上かかって進める事が出来た感じです。
    お陰様で一応CoreDataを使った簡単な部分は覚えれました(気がします)。
    もっとCoreDataを使った実践的なアプリを作れるように第二弾の方も通して勉強させて頂きたいのですが…

    少し解らない所が出たので質問させて下さい。

    >(各ViewControllerは簡略化のため、すべてUITableViewControllerのサブクラスにしてます。)
    と書かれていますのでViewControllerはUITableViewControllerのサブクラスと解り、
    Cellの方も理解出来ました。
    ただ、DTDValueCategorysがカテゴリ用クラスを何をサブクラスにして作成すれば良いのかが解らずに混乱してしまいました。
    宜しければ教えて下さい。

    >さらにcategoryのデフォルト値“No Category”を[#difine]しておけば
    失礼ですが、difineと誤字になってるみたいです。

    • 初めまして。コメントありがとうございます。

      まずは誤字のご指摘ありがとうございました。恥ずかしながら修正しておきました。

      ご質問のカテゴリの継承元についてですが、カテゴリですので継承元はありません。
      それと
      > ViewControllerはUITableViewControllerのサブクラスと解り…
      というのも違っていて、『TableViewControllerがViewControllerのサブクラス』になります。

      なかなか苦戦しながらも本エントリーを実践していらっしゃるようですが、カテゴリとは何か?やクラスの継承などObjective-Cの基礎的なことが理解できていないようでしたら、先にそちらの基礎知識を付けて頂いた方がスムーズかと思います。

      ご期待に添う返答ではないかとは思いますが、ご理解くださいませ

      • ご返信有り難う御座います。
        記載されていた該当するクラスの***ViewControllerと言うファイル名が
        UIViewControllerのサブクラスでは無くUITableViewControllerのサブクラスとと言うのがでしょうか…ちょっと伝え方が混乱してました。
        記載されていないとViewControllerと言うファイル名だったのでUIViewControllerで作成するのかなと言う感じだったもので。

        ただ、カテゴリと言うのを全く使った事もなく理解していなかったので一度そう言ったObjective-Cの基礎知識を付けながらStudy Core Dataの方も完走したいと思ってます。難しいですがじっくり時間掛けてやっていきたいのでまた質問等あればコメントするかもしれません。その時は宜しければお願い致します。

        アドバイス有り難う御座いました。

コメントを残す

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

WordPress.com ロゴ

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

Twitter 画像

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

Facebook の写真

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

Google+ フォト

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

%s と連携中