NSCalendar - 2つの日付間の日数を取得する
2010年7月31日土曜日 | Published in Code Snipet, iOS 4.0, iPhone | 2 コメント
実装
DateUtility というクラスを作り、そこへクラスメソッドを実装する。
@interface DateUtility : NSObject {
}
+ (NSInteger)daysBetween:(NSDate*)startDate and:(NSDate*)endDate;+ (NSDate*)adjustZeroClock:(NSDate*)date withCalendar:(NSCalendar*)calendar
{
NSDateComponents *components =
[calendar components:NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit
fromDate:date];
return [calendar dateFromComponents:components];
}
+ (NSInteger)daysBetween:(NSDate*)startDate and:(NSDate*)endDate
{
NSCalendar *calendar = [[NSCalendar alloc]
initWithCalendarIdentifier:NSGregorianCalendar];
startDate = [DateUtility adjustZeroClock:startDate withCalendar:calendar];
endDate = [DateUtility adjustZeroClock:endDate withCalendar:calendar];
NSDateComponents *components = [calendar components:NSDayCalendarUnit
fromDate:startDate
toDate:endDate
options:0];
NSInteger days = [components day];
[calendar release];
return days;
}NSCalendar を使うと2つの日付間の日数を簡単に取得できる。ポイントとしては +adjustZeroClock:withCalendar: を使い、時刻を 0:00 に合わせていること。これをやらないと日時まで含めた比較となってしまう。
(例)7/25 11:00〜7/27 9:00 のケース [A] 時刻補正なし:1日となる。 [B] 時刻補正あり:2日となる。補正が不要であれば +ajdustZeroClock:withCalendar: の呼出をやめる。
なお -[NSCalendar components:fromDate:toDate:options:] の最初に引数に NSMonthCalendarUnit を含めると、結果は日数ではなく月数+日数の組み合わせになるので注意が必要。
(例)7/10〜8/19の日のケース [A] NSDayCalendarUnit|NSMonthCalendarUnit [components month] == 1 [components day] == 10 [B] NSDayCalendarUnit [components day] == 40
参考情報
- NSDate Class Reference
- NSDateのリファレンス
- NSCalendar Class Reference
- NSCalendarのリファレンス
- NSDateComponents Class Reference
- NSDateComponentsのリファレンス
- Date and Time Programming Guide
- 日付関連クラスの解説
UITableView のヘッダの高さを変える
2010年7月30日金曜日 | Published in iOS 4.0, iPhone, UIKit, サンプルあり | 0 コメント
UITableView のヘッダの高さを変える
UITableView.tableHeaderView の frame.size.height を変えても反映されない。いろいろ試したところ tableHeaderView への再代入で反映されることがわかった。
GRect frame = self.headerView.frame; frame.size.height = 50.0; self.tableView.tableHeaderView.frame = frame; self.tableView.tableHeaderView = nil; self.tableView.tableHeaderView = self.headerView;ただし、同じものを代入しても駄目で一旦 nil を代入しておく。
トリッキーな方法だが現状これしか見つからなかった。
サンプル
アニメーションするサンプルを作ってみた。
こんなビューを用意して UITableView.tableHeaderView へセットしておく。初期状態では Area1 のみ表示させる。
実行するとこんな感じ。
Area2 がスルスルと開いていき
開き終わったら UITableView本体が再表示される。
"change header"ボタンが押された時の処理はこう。
- (IBAction)changeHeader:(id)sender
{
CGRect frame = self.headerView.frame;
if (headerOpened_) {
frame.size.height = 50.0;
} else {
frame.size.height = 100.0;
}
[UIView animateWithDuration:0.5
animations:^{self.tableView.tableHeaderView.frame = frame;}
completion:^(BOOL finished){
self.tableView.tableHeaderView = nil;
self.tableView.tableHeaderView = self.headerView;
}];
headerOpened_ = !headerOpened_;
}- - - -
ヘッダの拡大縮小に追随して同時に表の本体もアニメーションして欲しいのだがうまく行かなかった。ヘッダではなくセルで表現するしかないか。
ソースコード
GitHubからどうぞ。
TableHeader at 2010年07月26日 from xcatsan's iOS-Sample-Code - GitHub
CoreData - setFetchLimit:
2010年7月29日木曜日 | Published in Core Data, iOS 4.0, iPhone | 0 コメント
NSFetchRequest Class Reference
0 指定の場合は無制限となる。
発行されている SQLを見ると LIMIT が適用されているのがわかる。
CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZTIMESTAMP FROM ZEVENT t0 ORDER BY t0.ZTIMESTAMP DESC LIMIT 1
類似の設定として -[NSFetchRequest setFetchBatchSize:] がある。こちらは1度に取得する件数を指定する(例:5を指定した場合、10件のデータがある場合は2回SQLが飛ぶ)。
[参考情報] (旧) Cocoaの日々: CoreData - SQLite の LIMIT
UIBarButtonItem にカスタム画像を表示する
| Published in iOS 4.0, iPhone, UIKit | 2 コメント
initWithImage: を使うと白抜き表示になる?
UIBarButtonItem にはカスタム画像用に initWithImage:style:target:action: が用意されている。これを使えば簡単にカスタム画像をツールバーに表示できる、わけではなかった。
[[[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"Icon"] style:UIBarButtonItemStylePlain target:self action:@selector(openKarteList:)] autorelease];
普通にこんな画像を用意して
initWithImage: を使うと白抜きの表示になる。
ネットで探すとみんな困っているようだ。
UIBarButtonItem Image not showing - Mac Forums
Re: UIBarButtonItem image not showing up.... - msg#00180 - iPhoneSDKDevelopment
透明部分だけが利用される
UIBarButtonItem のリファレンスを読み直すと initWithImage:sytle:target:action:の説明にこう書いてあった。
[引用元] UIBarButtonItem Class Reference
The item’s image. Ifnilan image is not displayed.The images displayed on the bar are derived from this image. If this image is too large to fit on the bar, it is scaled to fit. Typically, the size of a toolbar and navigation bar image is 20 x 20 points. The alpha values in the source image are used to create the images—opaque values are ignored.
※下線は当ブログの著者が引いた。
どうも表示対象になるのは画像の透明部分(alpha値 < 1.0)のみで、不透明な部分は無視されるようだ。通常のアイコン画像は周辺は透明にするが、本体は不透明なのでこのルールに引っかかり白くなってしまっていた。
そうしたら本体部分の alpha値を下げてみよう。画像加工ソフト(Pixelmator)を使い全体の透明度(alpha値)を 0.5 に下げてみた。
どうだろうか。
やはりだめ。白抜きが灰色になっただけ。
そうか、alpha値だけが利用されるってことは、alphaレイヤーだけで絵を描かないとダメなのか。
Pixelmator だと描画内容をマスク(alphaレイヤー)に変換するフィルタがあるので、それを使ってみた。
こんなのができあがった。これをツールバーへ表示してみよう。
でた。
押すと自動的に光った効果が適用される。
なるほど、そういうことか。
他の方法
alphaレイヤーだけで描画した画像を作ればいいことがわかったが、普通の画像で表示する場合はどうすればいいのか。イニシャライザに -initWithCustomView: があるのでこれを使えばいいようだ。試しに UIImageView を充ててみた。
UIImageView* view = [[[UIImageView alloc] initWithImage: [UIImage imageNamed:@"Icon2"]] autorelease]; [buttons addObject:[[[UIBarButtonItem alloc] initWithCustomView:view] autorelease]];
すると出た。
ただ UIImageView は UIControlのサブクラスではないため、target-action によるイベントハンドリングは自前で実装する必要がある。また押した時にボタンが光る効果もない。
そう考えるとツールバーへ表示する画像はあらかじめ alphaレイヤーに描く用に変換したものを用意した方が無難なようだ。
参考情報
Pixelmator は Amazon.co.jp でも購入できるようです。
[フレーム]
英語がわかればオンラインでもっと安く購入できます。以下、過去に書いた購入記事です。
(旧) Cocoaの日々: Pixelmator 1.5 購入(割引クーポン適用)
NSFetchedResultsControllerDelegate - メモリ管理に関するメモ
2010年7月28日水曜日 | Published in Core Data, iOS 4.0, iPhone | 0 コメント
Delegateのメソッドはいつ呼ばれるのか?
- controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: は、NSManagedObjectContext に変化があった時に呼ばれる。
次のケースを想定してみる。
UINavigationController を使っていて、一覧画面から詳細画面へ移動してそこで NSManagedObjectContextに操作を加える。
ListViewController <NSFetchedResultsControllerDelegate> ⇒ (参照) UITableView* tableView ↓ DetailViewController ← NSManagedObjectContext操作(変更・削除など)すると ListViewController の -controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: が呼び出される。この時点ではまだ DetailViewController が表示されているものとする。
この Delegateメソッド内では通常 tableViewに対して操作を行っている。
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
:上記のケースでメモリ不足が発生して ListViewController の viewが 開放された場合、self.tableView へのアクセスが安全かどうかが気になる。
ListViewController.view が UITableView の場合
通常 ListViewController は UITableViewController のサブクラスとなる。UITableViewの管理は親クラスの tableViewインスタンスで管理される。
このケースは問題ない。
流れとしては次のようになる。
DetailViewController表示 ↓ メモリ不足発生 ↓ ListViewController で viewDidUnload が呼び出され、view(tableView) が開放される ↓ DetailViewController で NSManagedObjectContext を操作 ↓ ListViewController で controllerWillChangeContent: が呼び出される。 この中で self.tableView を参照。 ↓ self.tableViewへのアクセスをトリガーにして、ListViewController で viewDidLoad が呼び出される。これによって self.tableView の準備が完了する ↓ controllerWillChangeContent: の処理を実行 ↓ ListViewController の -controller:didChangeObject:atIndexPath:forChangeType: newIndexPath:呼び出しself.tableView への操作が無事に行われる ↓ ListViewController へ戻ると、変更が反映された表が表示されている。
ListViewController.view が UIView の場合
UIView の上に UITableView が載っているケース。この場合、ListViewController は UIViewController のサブクラスで tableView を定義して自前で管理する。
@interface RootViewController : UIViewController
<NSFetchedResultsControllerDelegate> {
UITableView* tableView_;
}
@property (nonatomic, retain) IBOutlet UITableView* tableView;
@end
@implementation RootViewController
:
- (void)viewDidUnload {
[super viewDidUnload];
self.tableView = nil;
}
:結論からすると、このケースも実質問題がない。
DetailViewController表示 ↓ メモリ不足発生 ↓ ListViewController で viewDidUnload が呼び出され、view が開放される ↓ この時 tableView も開放される(self.tableView=nil) ↓ DetailViewController で NSManagedObjectContext を操作 ↓ ListViewController の -controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:呼び出し ↓ self.tableViewに対してメッセージを送るが nil の為、何も起こらない ↓ ListViewControllerへ戻る ↓ viewが再ロードされ viewDidLoad が呼び出される。 UIViewControllerが参照するビューが芋づる式にロードされる。 ↓ UITableView は新規表示になるのでデータは最新のものを読み直し。 この結果、正しいデータが表示される。
ソースコード
両方のケースを試せる。ただし切り替えは手作業が少々必要(現在は1番目のケースで動作する)。
NSFetchedResultControllerDelegateSample at 2010年07月28日x from xcatsan's iOS-Sample-Code - GitHub
関連情報
Cocoaの日々: NSFetchedResultsControllerDelegate を使う
UIActionSheet を Blocks で処理する
| Published in Blocks, iOS 4.0, iPhone, UIKit, サンプルあり | 0 コメント
使い方はこんなイメージ:
ActionSheetBlocksExtension* sheet = [[ActionSheetBlocksExtension alloc]
initWithTitle:@"Action sheet sample"
didClick:^(UIActionSheet* actionSheet, NSInteger buttonIndex) {
NSLog(@"[1] index %d: %@", buttonIndex, actionSheet);
}
cancelButtonTitle:@"Cancel"
destructiveButtonTitle:@"Destructive"
otherButtonTitles:@"Other-11", @"Other-12", @"Other-13", nil];アーキテクチャ
UIActionSheet のサブクラスを作り、このクラスで UIActionSheetDelegate を実装する。できればカテゴリで行きたかったが、Delegate先になるのと Blockを保持する必要があったのでサブクラスとした。
実装
今回は ActionSheetBlocksExtension という UIActionSheet のサブクラスを作った。
@interface ActionSheetBlocksExtension : UIActionSheet <UIActionSheetDelegate> {
void (^didClickBlock_)(UIActionSheet*, NSInteger);
}
- (id)initWithTitle:(NSString *)title
didClick:(void (^)(UIActionSheet*, NSInteger))block
cancelButtonTitle:(NSString *)cancelButtonTitle
destructiveButtonTitle:(NSString *)destructiveButtonTitle
otherButtonTitles:(NSString *)firstOtherTitle,...;
@enddidClickBlock_ は引数で渡される Block を格納するためのメンバ変数。
実装:
#pragma mark -
#pragma mark Initialization & deallocation
- (id)initWithTitle:(NSString *)title
didClick:(void (^)(UIActionSheet*, NSInteger))block
cancelButtonTitle:(NSString *)cancelButtonTitle
destructiveButtonTitle:(NSString *)destructiveButtonTitle
otherButtonTitles:(NSString *)firstOtherTitle,...
{
self = [super initWithTitle:title
delegate:self
cancelButtonTitle:nil
destructiveButtonTitle:nil
otherButtonTitles:nil];
if (self) {
didClickBlock_ = [block retain];
int index = 0;
if (destructiveButtonTitle) {
[self addButtonWithTitle:destructiveButtonTitle];
self.destructiveButtonIndex = index;
index++;
}
if (firstOtherTitle) {
[self addButtonWithTitle:firstOtherTitle];
index++;
va_list args;
va_start(args, firstOtherTitle);
NSString* title;
while (title = va_arg(args, NSString*)) {
[self addButtonWithTitle:title];
index++;
}
va_end(args);
}
[self addButtonWithTitle:cancelButtonTitle];
self.cancelButtonIndex = index;
}
return self;
}
- (void) dealloc
{
[didClickBlock_ release];
[super dealloc];
}
#pragma mark -
#pragma mark UIActionSheetDelegate
- (void)actionSheet:(UIActionSheet*)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
{
didClickBlock_(actionSheet, buttonIndex);
}特に難しいことはやっていない。初期化で渡された blockを didClickBlock_ へ格納しておき、ボタンが押された時に呼び出している。
UIActionSheet のサブクラスの初期化については前回取り上げたのでそちらを参照のこと。
サンプル
これを組み込んだサンプルを作ってみた。呼び出し側のコードはこんな感じ。
- (IBAction)openSheet1:(id)sender
{
ActionSheetBlocksExtension* sheet = [[ActionSheetBlocksExtension alloc]
initWithTitle:@"Action sheet sample"
didClick:^(UIActionSheet* actionSheet, NSInteger buttonIndex) {
NSLog(@"[1] index %d: %@", buttonIndex, actionSheet);
}
cancelButtonTitle:@"Cancel"
destructiveButtonTitle:@"Destructive"
otherButtonTitles:@"Other-11", @"Other-12", @"Other-13", nil];
[sheet autorelease];
[sheet showInView:self.view];
}Blocks内の処理は、押されたボタンのindexとUIActionSheetのインスタンスをデバッグ出力している。さて実行してみよう。テスト用に3つの UIActionSheet を作るようにしてみた。
UIActionSheetの表示
ボタンを押すと Blocksで定義したコードが呼び出されているのがわかる。
[67822:207] [1] index 4: <ActionSheetBlocksExtension: 0x5d14ab0; baseClass = UIActionSheet; frame = (0 124; 320 336); opaque = NO; layer = <CALayer: 0x5d0fa80>>
ソースコード
GitHubからどうぞ。
ActionSheetUsingBlocks at 2010年07月27日 from xcatsan's iOS-Sample-Code - GitHub
参考情報
- Blocks Programming Topics: Getting Started with Blocks
- iPhone Dev Center 提供の Blocks解説
その他
Blocks は不慣れなので問題があるかもしれません。ツッコミがあれば是非コメントにどうぞ。
なお @marvelph @gnue のツイートが参考になりました。ありがとうございました。
UIActionSheet のサブクラス化
2010年7月27日火曜日 | Published in Code Snipet, iOS 4.0, iPhone, UIKit | 0 コメント
初期化メソッドのオーバーライド
可変引数はあとから -[addButtonWithTitle:]で追加してやる。
- (id)initWithTitle:(NSString *)title
delegate:(id < UIActionSheetDelegate >)delegate
cancelButtonTitle:(NSString *)cancelButtonTitle
destructiveButtonTitle:(NSString *)destructiveButtonTitle
otherButtonTitles:(NSString *)firstOtherTitle,...
{
self = [super initWithTitle:title
delegate:delegate
cancelButtonTitle:nil
destructiveButtonTitle:nil
otherButtonTitles:nil];
if (self) {
int index = 0;
if (destructiveButtonTitle) {
[self addButtonWithTitle:destructiveButtonTitle];
self.destructiveButtonIndex = index;
index++;
}
if (firstOtherTitle) {
[self addButtonWithTitle:firstOtherTitle];
index++;
va_list args;
va_start(args, firstOtherTitle);
NSString* title;
while (title = va_arg(args, NSString*)) {
[self addButtonWithTitle:title];
index++;
}
va_end(args);
}
[self addButtonWithTitle:cancelButtonTitle];
self.cancelButtonIndex = index;
}
return self;
}試しに呼び出すとこんな感じ。ちゃんと動いているようだ。
なお self=[super ...] で cancel と destructive ボタンを指定すると順番がおかしくなる。
これを防ぐために cancel と destructive ボタンも標準の順番に合うように追加している。
参考情報
- UIActionSheet addButtonWithTitle: doesn't add buttons in the right order - Stack Overflow
- 今回の方法が載っていた
- CodeResource - Uncategorized Messages - Uiactionsheet And Popviewcontrolleranimated
- 同様の方法が示されていた
- Cocoa with Love: Variable argument lists in Cocoa
- Cocoaにおける可変引数の記事
- Cocoaの日々 - 2005年1月
- 当ブログでも大昔に取り上げたことがあった。
- UIActionSheet Class Reference
- リファレンス
- - - -
次回は UIActionSheet の Blocks化をやります(今回はその伏線)。
UISearchDisplayController で用意される UITableView を扱う上での注意点
2010年7月26日月曜日 | Published in iOS 4.0, iPhone, UIKit | 0 コメント
[参考] Cocoaの日々: UISearchDisplayController と NSFetchedResultContoller を組み合わせる (3) 考察
さらに分かったことがあるので追記する。
[1] 検索毎に新規に UISearchResultsTableView が作り直される
-[UISearchDisplayControllerDelegate searchDisplayController:willShowSearchResultsTableView:] において引数で渡される UITableView をデバッグ出力したところ、検索毎に異なることがわかった。
[44712:207] <UISearchResultsTableView: 0x6044c00; ... [44712:207] <UISearchResultsTableView: 0x6099200; ... [44712:207] <UISearchResultsTableView: 0x6828e00; ...
つまり検索毎に作り直されている。通常表示(非検索時)用の UITableView の属性はコピーされないようなので、必要ならこのデリゲートメソッドで毎回属性の設定を行ってやる。
[例]
- (void)searchDisplayController:(UISearchDisplayController *)controller
willShowSearchResultsTableView:(UITableView *)tableView
{
tableView.backgroundColor = [UIColor clearColor];
tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
}[2] UISearchResultsTableView は UITableView の上に重なって表示される
UISearchResultsTableView は UITableView の上に重なって表示される。
この為、背景色を透明にしている場合は検索結果表示の下に通常表示のビューが重なって見えてしまう。
これでは困るので検索時には通常表示のビューを隠す必要がある。ただ単純に hidden=YES とすると、UISearchBarまでが非表示なってしまう。これは UISearchBar が通常表示ビュー UITableView のヘッダに描かれているため。ヘッダ以外の部分だけを隠せないものだろうか。
試しに UITableView の subviews を表示してみた。
[44878:207] ( "<UITableViewCell: 0x68a1e80; frame = (0 404; 320 72); ... "<UITableViewCell: 0x689cc90; frame = (0 332; 320 72); ... "<UITableViewCell: 0x6897ab0; frame = (0 260; 320 72); ... "<UITableViewCell: 0x6892800; frame = (0 188; 320 72); ... "<UITableViewCell: 0x688d410; frame = (0 116; 320 72); ... "<UITableViewCell: 0x6876600; frame = (0 44; 320 72); ... "<UISearchBar: 0x6862260; frame = (0 0; 320 44); ... "<UIImageView: 0x6863840; frame = (0 0; 7 7); ...なるほど。ヘッダと同じレベルで表示されているセルが1つずつ全部乗っているのか。
とすると、これらのセルを全部非表示にしてやる必要がありそうだ。ただそれをやると内部構造に依存するため今後の iOSのバージョンアップで非互換になる可能性がある。
色々考えたが、他に方法も無い(*1)のと恐らくこの構造が大きく変わることは無いと踏んで今回はセルを全部非表示にしてみる。
(*1) 今回背景色を透明にしているのは、ビュー階層の一番下に背景画像を表示しているからで、別の方法でこれが実現できれば内部構造に依存したコードを書く必要はなくなる。
まず現在表示されているセルの表示制御を行うメソッドを1つ用意する。
- (void)setDisplayedCellsHidden:(BOOL)hidden
{
for (UIView* view in self.tableView.subviews) {
if ([view isKindOfClass:[UITableViewCell class]]) {
view.hidden = hidden;
}
}
}これを検索開始と、検索終了のタイミングで呼び出す。
// 検索開始
- (void)searchDisplayController:(UISearchDisplayController *)controller
willShowSearchResultsTableView:(UITableView *)tableView
{
:
[self setDisplayedCellsHidden:YES];
}// 検索終了
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
{
:
[self setDisplayedCellsHidden:NO];
}
NSFetchedResultsControllerDelegate を使う
2010年7月25日日曜日 | Published in Core Data, iOS 4.0, iPhone | 0 コメント
今回は NSFetchedResultsControllerDelegate について調べた。
NSFetchedResultsControllerDelegate
NSFetchedResultsControllerDelegate Protocol Reference
NSFetchedResultsControllerDelegate は NSFetchedResultsController からのコールバックを受け取るためのメソッドが定義されているプロトコル。NSFetchedResutlsController は NSManagedObjectContext に対する操作(追加・変更・削除)を監視していて、それらを検出するとこのプロトコルのメソッドを呼び出す。この辺りは前回の Cocoaの日々: NSFetchedResultsController のおさらい にて少し触れた。
これらのメソッドの使い方は上記リファレンスの Overviewに書かれている。また Xcodeで Core Data を使うプロジェクトを作成すると、これらの実装コードが自動的に生成される。
利用パターン
利用パターンは3つ。
[A] 操作毎に処理するパターン
このパターンは NSManagedObjectContext の変更毎に呼び出されるメソッドを実装し、処理を行うパターン。Overviewではこれらが "Typical Use"として紹介されている。Xcodeが生成するコードもこのパターンになっている。
具体的には次のメソッドを実装する。
– controllerWillChangeContent: – controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: – controller:didChangeSection:atIndex:forChangeType: – controllerDidChangeContent:
通常はこれらの処理で UITableView に対する操作を行う。以下、controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: の実装例の引用:
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath]
atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
}
}操作毎に UITableView のアニメーション付きで表示更新を行うと、ユーザには一件づつ処理が行われているのが視覚的にわかるようになる。
[B] 操作終了時のみ処理するパターン
controllerDidChangeContent: のみ実装するパターン。これは処理対象の件数が多く [A]のパターンではパフォーマンス的に問題が出る場合に採用する。例えば100件のデータを削除するなど。1件づつ視覚的フィードバックを行うと非常に時間がかかるため現実的ではない。この場合は操作最後に UITableView の全件読み直しを行う方法が取れる。
[実装例]
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView reloadData];
}[C] デリゲートを利用しないパターン
NSFetchedResultsController.delegate = nil とするパターン。この場合は UITableView と NSManagedObjectContextとの同期を自前で処理する。
- - - -
通常は [A] を使い、処理件数が多い場合のみ [B] を使うといったハイブリッドなパターンも考えられる。
サンプルプログラム
動作確認するために複数の行を選択・削除できるサンプルプログラムを作ってみた。
[ソース] FetchedResultsControllerSample at 2010年07月25日 from xcatsan's iOS-Sample-Code - GitHub
行を複数選択して、右下のボタンを押すと選択された行がアニメーションしながら表から消える。
サンプルコードのベースは、Xcodeの "Navigation-based Application" + "Use Core Data for storage" を使った。モデルの定義と調整、行の複数選択以外は手を入れていない。NSFetchedResultsControllerDelegate の実装メソッドは Xcodeが生成した雛形をそのまま利用した。つまり [A]パターンの動作となる。これだけで最低限の処理ができるので便利だ。
なお試しに NSFetchedResultsControllerDelegate の実装メソッドを controllerDidChangeContent: だけにしてみた([B]パターン)。この場合は [A]パターンと違いアニメーションは起こらず、画面全体が読み直される。
参考情報
- Table View Programming Guide for iOS: Inserting and Deleting Rows and Sections
- "Batch Insertion, Deletion, and Reloading of Rows and Sections" UITableViewのバッチ操作についての解説。
NSFetchedResultsController のおさらい
2010年7月24日土曜日 | Published in Core Data | 0 コメント
データ取得
UITableView が表示するデータを UITableViewDataSource へ要求する。通常 UITableViewController がこのプロトコルを実装していて NSFetchedResultController から必要なデータを取得し、UITableViewへ返す。NSFetchedResultController は大抵の場合、UITableVIewController 初期化時にフェッチを行っていて、データ取得の要求があった場合は取得済みのデータを返す。
なお NSFetchedResultController の持つ cache はデータそのキャッシュではなく、section と ordering に関するもの。アプリケーションのレベルで管理される。
Where possible, a controller uses a cache to avoid the need to repeat work performed in setting up any sections and ordering the contents. The cache is maintained across launches of your application.
[参照] NSFetchedResultsController Class Reference
データ操作
データの追加や変更、削除は、NSFetchedResultsControllerではなく、NSManagedObjectContext に対して行う(② データ操作)。NSFetchedResultsController は NSManagedObjectContext を監視していて NSManagedObjectContext に変更が入った場合は NSFetchedResultsControllerDelegate へコールバックする(③ コールバック)。
コールバックされたらモデルの変更状況をビューである UITableView へ反映させる(④ 表示更新)。
- - -
Xcode で新規プロジェクトを作成する時に Navigation-based Application を選び、"Use Core Data for storage" にチェックを入れると、データの取得に加え、コールバックメソッドの処理の雛形を生成してくれる。
参考情報
- NSFetchedResultsController Class Reference
- リファレンス。Overview は簡潔だがわかりやすい。delegate と cache の話がまとめられている。
- NSFetchedResultsControllerDelegate
- コールバック用のプロトコル
下からせり上がってくる非モーダルなカスタムダイアログを作る (2)二段構え
2010年7月23日金曜日 | Published in GUI部品, iOS 4.0, iPhone, UIKit | 0 コメント
前回作成したカスタムダイアログに修正を入れて二段構えにする。
二段構え
二段構えとは、初期表示ではボタンのみ表示し、指示を受けるとラベルがせり上がってくることを指す。前回も取り上げた Pastebot がこのインターフェイスを採用している。
最初はこう。
表で行選択すると選択件数を表示するラベルが出てくる。
前回のコードに手を入れてこれを実現する。
実装
まず Interface Buidler で上部に配置していた Label を下へ隠す。
こうなる。
次に CustomDialogViewController に制御用のメソッドを追加する。このメソッドではラベルのアニメーション制御と共にボタンの有効向制御も行う。
- (void)setEnabled:(BOOL)enabled
{
if (enabled_ == enabled) {
return;
}
enabled_ = enabled;
self.button.enabled = enabled;
CGRect frame = self.label.frame;
if (enabled) {
frame.origin.y -= frame.size.height;
} else {
frame.origin.y += frame.size.height;
}
[UIView animateWithDuration:ANIMATION_DURATION
animations:^{self.label.frame = frame;}];
}やっていることは簡単で -[UIView animateWithDuration:animations:] を使い、ラベルの Y座標を変更しているだけ。Blocksが使えるようになったのでコードが非常にすっきりしている。実行結果
最初はこう。
ダイアログを開き
ラベルを開く
ソースコード
GitHubからどうぞ
DialogSample at 2010年07月23日b from xcatsan's iOS-Sample-Code - GitHub
下からせり上がってくる非モーダルなカスタムダイアログを作る
2010年7月22日木曜日 | Published in iOS 4.0, UIKit | 0 コメント
カスタムダイアログ
ユーザに操作指示を尋ねるために標準では UIActionSheetが用意されている。
ツールバーはボタンを赤くするにはカスタムな UIButton を貼り付ける必要があるようだ。
Pastebotの場合、さらにその上にメッセージを表示している。
今回はこれを実現する仕組みを作ってみた。
仕組み
UILabel と UIButton が乗った UIView を1枚用意して、アニメーションを使って下からせり上がるように表示する。実装
まずダイアログを管理する CustomDialogViewController を用意する。
#import "CustomDialogViewDelegate.h"
@interface CustomDialogViewController : UIViewController {
NSString* labelText_;
NSString* buttonTitle_;
id delegate_;
UILabel* label_;
UIButton* button_;
}
@property (nonatomic, copy) NSString* labelText;
@property (nonatomic, copy) NSString* buttonTitle;
@property (nonatomic, assign) id delegate;
@property (nonatomic, retain) IBOutlet UILabel* label;
@property (nonatomic, retain) IBOutlet UIButton* button;
-(IBAction)touchedButton:(id)sender;
@end ユーザインターフェイスは Interface Builder で作る。こんな感じ
コントローラを初期化するときにこの Nibを読み込む。
- (id)init {
if (self = [super initWithNibName:NSStringFromClass([self class])
bundle:nil]) {
}
return self;
}ラベルとボタンはインスタンス化のタイミングが表示される時になる。この為、表示する文字列は別途プロパティを用意し、先にここへいれておくようにする。インスタンス化された後(viewDidLoadのタイミング)に設定する。
- (void)viewDidLoad {
[super viewDidLoad];
[self.button setTitle:self.buttonTitle
forState:UIControlStateNormal];
self.label.text = self.labelText;
}ダイアログのボタンが押された時にはデリゲート先にメッセージを送る。その為にプロトコルを定義しておく。
@protocol CustomDialogViewDelegate -(void)touchedButton:(id)sender; @end
次にこのダイアログの表示制御を行うために UIViewController にカテゴリでメソッドを追加する。
@interface UIViewController (CustomDialog) - (void)presentDialogViewController:(UIViewController*)controller animated:(BOOL)animated; - (void)dismissDialogViewController:(UIViewController*)controller animated:(BOOL)animated; @end
上記は UIViewController 標準の presentModalViewController:animated:, dismissModalViewControllerAnimated: をまねた。標準と異なり、閉じるときにも UIViewController を必要とするのは、カテゴリではインスタンス変数を持てない為、状態管理ができないから。状態管理をやる場合はカテゴリではなくサブクラスにするといい。今回はお手軽に機能追加できるようにカテゴリの方法を採用した。
実装はこう。UIViewのアニメーション関連のメソッドを使っている。Blockが使えるのは非常に便利。
#pragma mark -
#pragma mark Manage dialog
- (BOOL)isExistSubView:(UIView*)view
{
BOOL is_exist = NO;
for (UIView* subview in self.view.subviews) {
if (view == subview) {
is_exist = YES;
break;
}
}
return is_exist;
}
- (void)presentDialogViewController:(UIViewController*)controller animated:(BOOL)animated
{
CGRect frame1 = self.view.frame;
CGRect frame2 = controller.view.frame;
// (1) init position
frame2.origin.y = frame1.size.height;
controller.view.frame = frame2;
if ([self isExistSubView:controller.view]) {
[self.view bringSubviewToFront:controller.view];
} else {
[self.view addSubview:controller.view];
}
// (2) animate
frame2.origin.y = frame1.size.height - frame2.size.height;
if (animated) {
[UIView animateWithDuration:0.5
animations:^{controller.view.frame = frame2;}];
} else {
controller.view.frame = frame2;
}
}
- (void)dismissDialogViewController:(UIViewController*)controller animated:(BOOL)animated
{
if (![self isExistSubView:controller.view]) {
return;
// do nothing
}
CGRect frame1 = self.view.frame;
CGRect frame2 = controller.view.frame;
// (1) animate
frame2.origin.y = frame1.size.height;
if (animated) {
[UIView animateWithDuration:0.5
animations:^{controller.view.frame = frame2;}
completion:^(BOOL finished){[controller.view removeFromSuperview];}
];
} else {
[controller.view removeFromSuperview];
}
}
@end最後にこれを使うクライアントコード。まず初期化。
- (void)viewDidLoad {
[super viewDidLoad];
CustomDialogViewController* controller =
[[CustomDialogViewController alloc] init];
controller.delegate = self;
controller.buttonTitle = @"close dilaog";
controller.labelText = @"Custom Dialog Opened!";
self.dialogViewController = controller;
[controller release];
}次にダイアログの開閉。
- (IBAction)openDialog:(id)sender
{
if (opened) {
[self dismissDialogViewController:self.dialogViewController
animated:YES];
} else {
[self presentDialogViewController:self.dialogViewController
animated:YES];
}
opened = !opened;
}ダイアログボタンが押された時のアラート表示。
-(void)touchedButton:(id)sender
{
UIAlertView* alert = [[[UIAlertView alloc] initWithTitle:@"Message"
message:@"Touched dialog button"
delegate:nil
cancelButtonTitle:nil
otherButtonTitles:@"OK", nil] autorelease];
[alert show];
}実行結果
さて実行してみよう。初期状態。
ボタンを押すと下からダイアログがせり上がってくる。
ダイアログのボタンを押すと -[CustomDialogDelegate touchedButton:] が呼ばれ、アラートが表示される。
もう一度ボタンを押すとこんどはアニメーションしながらダイアログが下へ消える。
ソースコード
DialogSample at 2010年07月23日 from xcatsan's iOS-Sample-Code - GitHub
UISearchDisplayController と NSFetchedResultContoller を組み合わせる (3) 考察
2010年7月21日水曜日 | Published in Core Data, iOS 4.0, iPhone, UIKit | 0 コメント
状態
UISearchDisplayController を使った場合の状態イメージはこう。
表示用のビューは UITableView と UISearchResultsTableView の2つが、状態によって切り替わるようにできている。
通常は1つの UITableViewController に対して、これら2つのビューを結びつける(DataSource/Delegate)ことになる。
この為、Data Source / Delegate のメソッド内では必要に応じて、どちらのビューを扱っているのかを判断する。
[例]
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if (tableView == self.searchDisplayController.searchResultsTableView)
{
return [self.filteredListContent count];
}
else
{
return [self.listContent count];
}
}またモデルである NSFetchedResultsController を1つだけ用意して両方の状態で利用する場合は、検索時には条件の設定を、検索後には条件クリア(全件)を行う必要がある。
[例]
#pragma mark -
#pragma mark UISearchDisplayController Delegate
- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope
{
NSString *query = self.searchDisplayController.searchBar.text;
if (query && query.length) {
NSPredicate *predicate =
[NSPredicate predicateWithFormat:@"Title contains[cd] %@", query];
[self.fetchedResultsController.fetchRequest setPredicate:predicate];
}
[self reloadFetchedResultsController];
}
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller
shouldReloadTableForSearchString:(NSString *)searchString
{
[self filterContentForSearchText:searchString scope:
[[self.searchDisplayController.searchBar scopeButtonTitles]
objectAtIndex:[self.searchDisplayController.searchBar
selectedScopeButtonIndex]]];
return YES;
}
#pragma mark -
#pragma mark UISearchBar Delegate
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
{
[self.fetchedResultsController.fetchRequest setPredicate:nil];
[self reloadFetchedResultsController];
}なお iPhone付属の iPod では、検索時には別途 UITableView が用意される性質を利用して、検索結果の表示をカスタマイズしている。iPhodの場合、アーティストやアルバムでグルーピングし、Section毎に結果を表示している。
参考情報
Cocoaの日々: UISearchDisplayController 調査Cocoaの日々: UISearchDisplayController と NSFetchedResultContoller を組み合わせる
Cocoaの日々: UISearchDisplayController と NSFetchedResultContoller を組み合わせる (2) バグ修正
UISearchDisplayController と NSFetchedResultContoller を組み合わせる (2) バグ修正
2010年7月20日火曜日 | Published in Core Graphics, iOS 4.0, iPhone, UIKit | 0 コメント
前回のコードに問題があることがわかった。
バグ
画面からはみ出る程度に件数を増やすと次のケースでクラッシュすることがわかった。
(1) 初期表示
(2) 検索実行
(3) キャンセルで元の一覧戻り、下へスクロール
⇒ クラッシュ原因
検索時の NSFetchedResultController が、検索後に元の画面へ戻った時に使用されていた。どういうことかというと、元々10件のデータがあったにもかかわらず、検索で絞り込まれて1件となった後、元の画面に戻ってもその1件のままとなっていた。その状態で下へスクロールして見えていなかったデータを表示しようとした時に -tableView:cellForRowAtIndexPath: が呼び出され、そこで NSFetchedResultController に存在しない Indexを指定してエラーとなっていた(例:結果が1件にもかかわらずスクロールによって現れた 10件目のデータへアクセスしようとしていた)。
元の画面に戻った時に NSFetchedResultController の検索条件を元に(全件)戻し、フェッチをやり直す必要がある。
修正
検索完了時に NSFetchedResultController の検索条件を元に戻し、再フェッチする。UISearchBarの「キャンセル」ボタンが押された時を検索完了とすると UISearchBarDelegate のメソッドにこれらの処理を記述できる。
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
{
[self.fetchedResultsController.fetchRequest setPredicate:nil];
[self reloadFetchedResultsController];
}条件をクリア(nil)した上で再フェッチを行う。-(void)reloadFetchedResultsController {
NSError *error = nil;
[NSFetchedResultsController deleteCacheWithName:@"UserSearch"];
if (![self.fetchedResultsController performFetch:&error]) {
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
}ソースコード
SearchSample at 2010年07月20日 from xcatsan's iOS-Sample-Code - GitHub
Core Data - 最大値を取得する
2010年7月19日月曜日 | Published in Code Snipet, Core Data | 0 コメント
SQL だと
SELECT MAX(timeStamp) FROM Book;と、たった一行で簡単に取得できるが Core Data ではどうか?
前提
こんなエンティティがあったとする。
この属性値 timeStamp の最大値(すなわち最も最近の日時)を取得するメソッドを用意する。またこのメソッドは絞り込みの条件として Author(NSManagedObjectのサブクラス)を渡すことができる。
コード見本
こんな感じ。
- (Book*)lastTimeStampOfAuthor:(Author*)author
{
NSManagedObjectContext* moc = self.managedObjectContext;
NSFetchRequest* request = [[NSFetchRequest alloc] init];
// entity
NSEntityDescription* entity = [NSEntityDescription entityForName:@"Book"
inManagedObjectContext:moc];
[request setEntity:entity];
// expression
NSExpression *keyPathExpression = [NSExpression expressionForKeyPath:@"timeStamp"];
NSExpression *expression =
[NSExpression expressionForFunction:@"max:"
arguments:[NSArray arrayWithObject:keyPathExpression]];
// expresssion description
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
[expressionDescription setName:@"maxTimeStamp"];
[expressionDescription setExpression:expression];
[expressionDescription setExpressionResultType:NSDateAttributeType];
// result properties
[request setResultType:NSDictionaryResultType];
[request setPropertiesToFetch:[NSArray arrayWithObject:expressionDescription]];
// predicate
if (author) {
NSPredicate* predicate = [NSPredicate predicateWithFormat:@"Author == %@", author];
[request setPredicate:predicate];
}
// execution
NSError* error = nil;
NSArray* array = [moc executeFetchRequest:request error:&error];
NSDate* timeStamp = nil;
if (error) {
NSLog(@"[ERROR] %@", error);
} else {
timeStamp = [[array objectAtIndex:0] valueForKey:@"maxTimeStamp"];
}
[expressionDescription release];
[request release];
return timeStamp;
}長っ...
参考情報
Core Data Programming Guide: Fetching Managed Objects - Fetching Specific Values
補足
(7/27補足)SQLを確認したところ次のようになっていた。CoreData: sql: SELECT max( t0.ZTREATEDDATE) FROM ZKARTE t0 WHERE t0.ZCUSTOMER = ?
やっぱり1行か。
Xcode - 矩形選択
2010年7月18日日曜日 | Published in Tips, Xcode | 0 コメント
optionキーを押すとマウスカーソルが+になるので、その状態で矩形の範囲を選択する。
貼り付けるとそこへコピーした時の矩形範囲そのままで挿入される。
UITableViewController は initWithStyle: で UITableView を作成する
2010年7月17日土曜日 | Published in iOS 4.0, iPhone, UIKit | 0 コメント
ものすごい勘違いをしていてハマった。記録に残しておく。
TableViewController.m
TableViewController.h
TableViewController.xib
を用意しておき、他のコントローラからこのコントローラを呼び出す。この時 -init を使う。
TableViewController *tableViewController = [[TableViewController alloc] init]; [self.navigationController pushViewController:tableViewController animated:YES]; [tableViewController release];
-initはこんな実装。
- (id)init
{
if (self = [super initWithStyle:UITableViewStylePlain]) {
:
:
}
return self;
}この場合、TableViewController.xib は使われない。なぜなら initWithStyle: を呼んだ場合、UITableViewController が UITableView を自動的に生成するから。普通に考えればわざわざ UITableViewStyle を引数に取っているので自明ではある..。
ずっと xib が使われているものだと思ってそのままにしていた。今回たまたま検索窓をつけようとしてこのことに気がついた。最初はなぜ xibの変更が反映されないのか焦ってしまった。
解決策は UIViewController の initWithNibName:bundle: を使うこと。
- (id)init
{
if (self = [super initWithNibName:@"TableViewController" bundle:nil]) {
:
:
}
return self;
}もしくは呼び出し側で initの代わりに使う。Xcodeが生成するテンプレートはこちらのスタイルになっている。
TableViewController *tableViewController = [[TableViewController alloc] initWithNibName:@"TableViewController" bundle:nil]; [self.navigationController pushViewController:tableViewController animated:YES]; [tableViewController release];
UISearchDisplayBar を初期状態では隠しておく
2010年7月16日金曜日 | Published in iOS 4.0, iPhone, UIKit | 0 コメント
UISearchDisplayControllerのサーチバーを最初は隠しておきたい « Programmer’s High
コード(引用)
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.contentOffset = CGPointMake(0.0, self.searchDisplayController.searchBar.bounds.size.height);
}
前回のコードへ加えてみた。
初期表示
下へ引っ張ると出てくる
いい感じだ。
人気の投稿(過去 30日間)
-
公式リファレンスの Q&A に解説がある。 Technical Q&A QA1551: Detecting the start and end edit sessions of a cell in NSTableView. 方法は Delegate と Not...
-
2011年06月09日 追記 UITableViewCell の Identifier 設定を忘れてたので追記しました。 UINib を使うと簡単に Nib で定義した UITableViewCell が使える。 今回のサンプル: [関連] Cocoaの日々: [iO...
-
Asset Catalogには画像以外のデータも置ける。サウンドファイル(.aif)を置いてみた。 取り出すには NSDataAsset を使う。 let sound = NSDataAsset(name: name) // use sound.data 取り出したサウ...
-
パスワードを暗号化して安全に iPhone/iPad へ保管したい。iOS はこの用途の為に Keychain Services を提供している。今回は Keychain Services について調べてみた。リファレンスの内容に加え、独自に調査・検証した結果をまとめてある。動作...
-
Core Data を使ったアプリケーションで下のような検索機能を実装している。 設定された値を元に NSPredicate を作成し、Core Data に対して検索をかけるのだが、こういう場合に NSCompoundPredicate が役に立つ。 NSCompound...