NSCalendar - 2つの日付間の日数を取得する

2010年7月31日土曜日 | Published in | 2 コメント

このエントリーをはてなブックマークに追加

例えば 2010/07/25 と 7/27 の間の日数は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 | 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 | 0 コメント

このエントリーをはてなブックマークに追加

-[NSFetchRequest setFetchLimit:1] とすると1件だけ結果を取り出すことができる。

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 | 2 コメント

このエントリーをはてなブックマークに追加

UIBarButtonItem に指定した画像を表示する。こんな感じ。


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:の説明にこう書いてあった。


The item’s image. If nil an 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.
[引用元] UIBarButtonItem Class Reference
※下線は当ブログの著者が引いた。

どうも表示対象になるのは画像の透明部分(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 | 0 コメント

このエントリーをはてなブックマークに追加

NSFetchedResultsController を使っていて、NSFetchedResultsControllerDelegate を実装している時のメモリ管理に関する私的メモ。


Delegateのメソッドはいつ呼ばれるのか?


- controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: は、NSManagedObjectContext に変化があった時に呼ばれる。

次のケースを想定してみる。

UINavigationController を使っていて、一覧画面から詳細画面へ移動してそこで NSManagedObjectContextに操作を加える。
ListViewController &ltNSFetchedResultsControllerDelegate> ⇒ (参照) 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 へ戻ると、変更が反映された表が表示されている。

UITableViewController を使う場合、self.view と self.tableView は等価となる。このため self.tableViewに対するアクセスによって、self.view に仕掛けられた KVO機構が発動して UITableViewがロードされ self.tableView で使えるようになると思われる(※これは推測)。=> KVOなんて使わなくて単なる getterメソッドの実装でそうなっている、と思われる。


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-28x from xcatsan's iOS-Sample-Code - GitHub


関連情報


Cocoaの日々: NSFetchedResultsControllerDelegate を使う

UIActionSheet を Blocks で処理する

| Published in | 0 コメント

このエントリーをはてなブックマークに追加

iOS4から UIView のアニメーション関連のメソッドで Blocksが利用できるようになった。最近よく利用しているがこれは非常に便利。一方、UIActionSheet で不便を感じていたので試しに Blocksが使える実装を書いてみた。

使い方はこんなイメージ:
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,...;

@end

didClickBlock_ は引数で渡される 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 | 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 | 0 コメント

このエントリーをはてなブックマークに追加

以前、UISearchDisplayController についてのまとめを書いた。
[参考] 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 | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: NSFetchedResultsController のおさらい

今回は 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 | 0 コメント

このエントリーをはてなブックマークに追加

改めて調べなおした。NSFetchedResultsController はデータ取得と操作(挿入・変更・削除)に分けて考えるとわかりやすい。以下、UITableView および UITableViewController と組み合わせた典型的なパターンについて説明する。


データ取得


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 | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: 下からせり上がってくる非モーダルなカスタムダイアログを作る

前回作成したカスタムダイアログに修正を入れて二段構えにする。

二段構え


二段構えとは、初期表示ではボタンのみ表示し、指示を受けるとラベルがせり上がってくることを指す。前回も取り上げた 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-23b from xcatsan's iOS-Sample-Code - GitHub

下からせり上がってくる非モーダルなカスタムダイアログを作る

2010年7月22日木曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

カスタムダイアログ


ユーザに操作指示を尋ねるために標準では UIActionSheetが用意されている。

UIActionSheetはモーダルなので、非モーダルにする場合は通常ツールバーを使う。

下は Pastebotの例。



ツールバーはボタンを赤くするにはカスタムな 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 | 0 コメント

このエントリーをはてなブックマークに追加

UISearchDisplayController と NSFetchedResultContoller を使う場合のメモなど。


状態


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 | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: UISearchDisplayController と NSFetchedResultContoller を組み合わせる

前回のコードに問題があることがわかった。

バグ


画面からはみ出る程度に件数を増やすと次のケースでクラッシュすることがわかった。

(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 | 0 コメント

このエントリーをはてなブックマークに追加

Core Data に格納されたデータの中から特定の属性値が最大値を取得したい。
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 | 0 コメント

このエントリーをはてなブックマークに追加

Xcode では option キーを押しながらマウスで矩形選択ができる。

optionキーを押すとマウスカーソルが+になるので、その状態で矩形の範囲を選択する。

貼り付けるとそこへコピーした時の矩形範囲そのままで挿入される。

UITableViewController は initWithStyle: で UITableView を作成する

2010年7月17日土曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

UITableViewController Class Reference

ものすごい勘違いをしていてハマった。記録に残しておく。

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 | 0 コメント

このエントリーをはてなブックマークに追加

ずばりの記事があった。
UISearchDisplayControllerのサーチバーを最初は隠しておきたい « Programmer’s High

コード(引用)
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.contentOffset = CGPointMake(0.0, self.searchDisplayController.searchBar.bounds.size.height);
}


前回のコードへ加えてみた。

初期表示

下へ引っ張ると出てくる



いい感じだ。

人気の投稿(過去 30日間)