UISearchDisplayController 調査

2010年7月14日水曜日 | Published in | 0 コメント

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

NSFetchedResultController を使った UITableView に検索機能を付加したい。今回はまず UISearchDisplayController について調べてみた。

サンプル - TableSearch


UISearchDisplayController を使ったサンプルが iPhone Dev Center で提供されている。
TableSearch

こんな画面。検索窓に入力するとリアルタムに検索結果が表示される。


Core Data は使っていない。
今回はこのソースコードを読んでみる。


MainView.xib


UITableView が配置されていてそのヘッダに UISearchBar が入っている。Interface Builder でこんなことができるのか。Libraryウィンドウを見ると "Search Bar and Search Display Controller"というのがある。

これを UITableView の上へドラッグするとヘッダの辺りが反応して配置ができることがわかる。

UISearchBar が UITableViewのサブビュー(ヘッダ)に配置され、UISearchDisplayController のインスタンスが準備される。

さてサンプルへ戻り各オブジェクトの接続状況を見てみる。まずは UITableView から。
dataSource と delegate が接続されていないことがわかる。あとで見るようにこれらは UISearchDisplayController 側で管理するようだ。

次に UISearchBar
delegate(UISearchBarDelegate)が File's Owner へ接続されている。

UISearchDisplayController
様々な Outlets が用意されている。これらは Interface Builder で配置すると自動的に接続されるようだ。

最後に File's Owner 。これは UITableViewController のサブクラス。
searchDisplayController への Outlet が接続されている。これは UIViewController が持っているプロパティ(つまり UIViewController 自体が UISearchDisplayController に対応しているということか)。


MainViewController.h

メインとなるコントローラで UITableViewController のサブクラス。ソースはこちら。
TableSearch: MainViewController.h

定義部:
@interface MainViewController : UITableViewController 


MainViewController.m

実装コード。ソースはこちら。
TableSearch: MainViewController.m

メソッドはこんな感じ。
大きくは UIViewController の処理(Lifecycle methods)、UITableView 向けのデータソース/デリゲート、検索絞り込み、UISearchDisplayControllerのデリゲートから構成されている。

気になった部分をピックアップしてみる。

まず viewDidDisapear
- (void)viewDidDisappear:(BOOL)animated
{
    // save the state of the search UI so that it can be restored if the view is re-created
    self.searchWasActive = [self.searchDisplayController isActive];
    self.savedSearchTerm = [self.searchDisplayController.searchBar text];
    self.savedScopeButtonIndex = [self.searchDisplayController.searchBar selectedScopeButtonIndex];
}
ビューが隠れるときに UISearchDisplayController関連のプロパティを保存している。これは viewDidLoad で再利用される。メモリ不足時に viewが再作成されることを想定したパターン。
- (void)viewDidLoad
{
  :
  :
 // restore search settings if they were saved in didReceiveMemoryWarning.
    if (self.savedSearchTerm)
 {
        [self.searchDisplayController setActive:self.searchWasActive];
        [self.searchDisplayController.searchBar setSelectedScopeButtonIndex:self.savedScopeButtonIndex];
        [self.searchDisplayController.searchBar setText:savedSearchTerm];
        
        self.savedSearchTerm = nil;
    }
  :
  :

次は UITableViewDataSource 関連
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
 if (tableView == self.searchDisplayController.searchResultsTableView)
 {
        return [self.filteredListContent count];
    }
 else
 {
        return [self.listContent count];
    }
}

tableView によって返す値を分けている。通常時と検索時で渡される UITableView のインスタンスが異なるようだ。試しに self.tableView と 引数で渡ってくる tabaleView をデバッグ表示してみた。

通常(非検索)
self.tableView: <UITableView: 0x6829a00;
2: <UITableView: 0x6829a00;
UITableView のインスタンスが渡ってきている。

検索時
self.tableView: <UITableView: 0x6829a00;
1: <UISearchResultsTableView: 0x6044600;
self.tableView: <UITableView: 0x6829a00;
1: <UISearchResultsTableView: 0x6044600;
    :
UISearchResultsTableView のインスタンスが渡ってきている。このクラスは非公開で UIKit 内部だけで使われるクラスのようだ。

他のメソッドでも同様に tableView を元に表示処理の振り分けを行っている。
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
 static NSString *kCellID = @"cellID";
 
 UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellID];
 if (cell == nil)
 {
            :
    }
 
 Product *product = nil;
 if (tableView == self.searchDisplayController.searchResultsTableView)
 {
        product = [self.filteredListContent objectAtIndex:indexPath.row];
    }
 else
 {
        product = [self.listContent objectAtIndex:indexPath.row];
    }
            :
            :

UITableViewDelegate も同様。
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    UIViewController *detailsViewController = [[UIViewController alloc] init];
    
 Product *product = nil;
 if (tableView == self.searchDisplayController.searchResultsTableView)
 {
        product = [self.filteredListContent objectAtIndex:indexPath.row];
    }
 else
 {
        product = [self.listContent objectAtIndex:indexPath.row];
    }
 detailsViewController.title = product.name;
    
    [[self navigationController] pushViewController:detailsViewController animated:YES];
    [detailsViewController release];
}

残りは UISearchDisplayControllerDelegate
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString
{
    [self filterContentForSearchText:searchString scope:
   [[self.searchDisplayController.searchBar scopeButtonTitles] objectAtIndex:[self.searchDisplayController.searchBar selectedScopeButtonIndex]]];
    
    // Return YES to cause the search result table view to be reloaded.
    return YES;
}

- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchScope:(NSInteger)searchOption
{
    [self filterContentForSearchText:[self.searchDisplayController.searchBar text] scope:
   [[self.searchDisplayController.searchBar scopeButtonTitles] objectAtIndex:searchOption]];
    
    // Return YES to cause the search result table view to be reloaded.
    return YES;
}
どちらもこのクラスで定義した filterContentForSearchText:scope: を呼び出す。このメソッドは検索条件に応じて self.filteredListContent をつくり直す。


- - - -
大まか理解できた。次は NSFetchedResultController との連携について調べる。

Xcode - Build And Analyze 〜 問題の例

2010年7月13日火曜日 | Published in | 0 コメント

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

[前回] Cocoaの日々: Xcode - Build And Analyze

Build And Analyze で指摘されたケースについて記録を残しておく。


Potential leak of an object allocated


メモリリークのケース

開放わすれ(NS系オブジェクト)


Appleのサンプルからコピーしてきたものだが NSCalendar* gregorian の開放ができていなかった。return前に開放コードを追加する必要がある。
[gregorian release];


開放忘れ(CG系関数)


CGImageCreateWithMask()で生成した CGImageRef が開放されていない。次のように書き直す。

CGImageRef maskImage = CGImageCreateWithMask(grayScaleImage, alphaImage);
 UIImage* grayScaleUIImage = [UIImage imageWithCGImage:maskImage];
 CGImageRelease(maskImage);


開放忘れ(UIViewController)


単純な開放忘れ。最後に開放コードを追加する。
[viewController release];


undefined or garbage value


Undefined or garbage value returned to caller


これは cell が初期化されていないのと、switch-case文で default: が定義されていないことによる。今回は下記のコードを追加した。
UITableViewCell* cell = nil;


Pass-by-value argument in message expression is undefined


これもauto変数が初期化されていないのと、switch-case文で default: が定義されていないことによる。今回は下記のコードを追加した。
EditViewController* viewController = nil;;

The left operand of '<' is a garbage value


height が初期化されていない。初期化処理を追加する。
CGFloat height = 0;



これで無事完了。

Xcode - Build And Analyze

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

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

話には聞いていたが使ったことがなかったので試してみた。


すると、あるわあるわ。ひどいもんだ。。
イージーミスが多い。


これは便利。今後は必須としたい。

Subversion で @ を含むファイルを扱う

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

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

iOS4 から導入された画像ファイルの命名規約では '@' を使う。

[参考] Cocoaの日々: iOS4からデバイス毎・解像度毎に用意した画像を自動選択する仕組みが導入された

この @ を名前に含むファイルを Subversion で扱う場合、普通にやるとエラーで蹴られる。

例えば
$ svn add Image001@2x.png
svn: warning: 'Image001' not found

$ svn add Image*
svn: warning: 'Image001' not found
svn: warning: 'Image002' not found
 :

これは svnで @は特別な意味を持っているため。

こうすればいい。
$ svn add 'Image0001@2x.png'@HEAD
A  (bin)  Image0001@2x.png

まとめてやるなら
$ for file in Image*2x.png; do svn add $file@HEAD; done


参考情報


svnでファイル名に@が含まれるファイルをrevert/rmする方法 - それ、Gentooだとどうなる?
今回参考になった

UIViewController でのメモリ管理見本

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

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

[2010-10-19訂正] [a][b]のケースでも deallocでの解放が必要なことが判明。それに適した記述に訂正してあります。
[関連情報] Cocoaの日々: viewDidUnload は呼ばれない(メモリ不足時だけ呼ばれる)


UIViewController 内で使うオブジェクトのメモリ管理(作成と開放のタイミング)と初期設定についてまとめてみた。

実装見本


4種類のインスタンス変数をもつケースについて考えてみる。
[a] nibから生成されるコントロール
[b] 実行時に作成するコントロール
[c] 他クラスへの公開プロパティ
[d] 内部で使うインスタンス変数

ヘッダはこんな感じ。
@interface SampleViewController : UIViewController {

 UITextField* textField;    // [a] nibから生成されるコントロール
 UIImageView* imageView;    // [b] 実行時に作成するコントロール
 NSString* name;            // [c] 他クラスへの公開プロパティ
 NSMutableArray* history;   // [d] 内部で使うインスタンス変数
}

@property (nonatomic, retain) IBOutlet UITextField* textField;
@property (nonatomic, retain) UIImageView* imageView;
@property (nonatomic, retain) NSString* name;

@end

続いて実装。メモリ管理にのみフォーカスして書いたコードなので意味のある処理は行っていない。
#import "SampleViewController"

@implementation SampleViewController

@synthesize textField;
@synthesize name;
@synthesize imageView;

- (id)init
{
 if (self = [super init]) {
  // [c] 他クラスへの公開プロパティ [作成][初期設定]
  self.name = @"no name";

  // [d] 内部で使うインスタンス変数 [作成][初期設定]
  history = [[NSMutableArray alloc] init];
 }
 return self;
}

- (void)viewDidLoad {
 [super viewDidLoad];

 // [a] nibから生成されるコントロール [初期設定]
 self.textField.text = @"(non)";

 // [b] 実行時に作成するコントロール [作成][初期化]
 self.imageView = [[[UIImageView alloc]
  initWithImage:[UIImage imageNamed:@"sample1.png"]] autorelease];
 [self.view addSubview:self.imageView];
}

- (void)viewWillAppear:(BOOL)animated {
 [super viewWillAppear:animated];
 self.textField.text = self.name;
}


- (void)viewDidUnload {
 self.textField = nil;  // [a] nibから生成されるコントロール [開放]
 self.imageView = nil;  // [b] 実行時に作成するコントロール [開放]
}


- (void)dealloc {
 self.textField = nil;  // [a] nibから生成されるコントロール [開放]
 self.imageView = nil;  // [b] 実行時に作成するコントロール [開放]
 self.name = nil;       // [c] 他クラスへの公開プロパティ [開放]
 [history release];     // [d] 内部で使うインスタンス変数 [開放]
 [super dealloc];
}

@end

4種類のインスタンス変数が参照するオブジェクトはそれぞれの生成タイミングの違いから、開放タイミングも異なる。順に追って見る。

[a] nibから生成されるコントロール


[作成] UIKitが自動的に nibから読み込んでメモリ内にインスタンス化する。その後 UIViewControllerのインスタンス変数へ設定してくれる。

[初期設定] 通常 InterfaceBuilderのインスペクタを使って設定を行うが、プログラムで初期設定を行う場合は viewDidLoadで行う。

[開放] viewDidUnload と dealloc の両方で行う。

コントロールの親viewはメモリ不足になると一旦開放されることがある。この為、UIViewControllerに比べて生存期間が短い上に、何度も作成と破棄が繰り返される場合が起こりうる。viewの開放タイミング(viewDidUnload)で、開放を行う。


[b] 実行時に作成するコントロール


[作成] viewと連携するコントロールの場合は、viewがインスタンス化して使えるようになっている必要がある。この為、作成は viewが使えるようになった時点、すなわち viewDidLoad で行う。

[初期設定] 作成と同様 viewDidLoad で行う。

[開放] viewDidUnLoad と dealloc の両方で行う。

作成を UIViewController の初期化タイミングで行うケースもある。この場合、開放は dealloc で行う。


[c] 他クラスへの公開プロパティ


[作成] 作成は UIViewController の初期化タイミングで行う。作成しないこともある(初期値 nilの場合など)。

[初期設定] 作成と同様。

[開放] dealloc で行う。


[d] 内部で使うインスタンス変数


[作成] 作成は UIViewController の初期化タイミングで行う。そうでないとクラス内で利用できない。

[初期設定] 作成と同様。

[開放] dealloc で行う。


まとめ


以上を表にまとめると次のようになる。

種類 init* viewDidLoad viewDidUnLoad dealloc
[a] nibから生成される
コントロール
- [初期設定] [開放] [開放]
[b] 実行時に作成する
コントロール
- [作成]
[初期設定]
[開放] [開放]
[c] 他クラスへの
公開プロパティ
[作成]
[初期化]
- - [開放]
[d] 内部で使う
インスタンス変数
[作成]
[初期化]
- - [開放]


メモリ開放の基本ルール


  • nib 内にあるコントロールの開放は viewDidUnloadとdealloc両方で行う
  • viewDidLoad で作成したコントロールの開放は viewDidUnloadとdealloc両方で行う
  • init* で作成したインスタンスの開放は deallocで行う
  • 他クラスから代入されたプロパティの開放は deallocで行う


その他


UIViewControllerの初期化


UINavigationBar を使っている場合は大抵の場合 init を使うので initで初期化を行う。一方、nibから UIViewController を作成する場合は initの代わりに initWithCoder: で初期化を行う(initや initWithNibName:bundle: は呼び出されない)。


メモリ不足


メモリ不足になった場合に didReceiveMemoryWarning が呼ばれる。この時、インスタンス変数で大きなサイズのデータを扱っている場合は必要に応じて開放する。次の2つが対象。
[c] 他クラスへの公開プロパティ
[d] 内部で使うインスタンス変数

viewに紐付けられる [a][b]などは、viewが開放された時に viewDidUnload が呼び出されるのでそのタイミングで開放される(このブログで示した実装になっているとして)。




参考情報

Resource Programming Guide: Nib Files
Nib経由でインスタンス化されるオブジェクトの初期化順序が解説されている。
Cocoaの日々: UIView上のコントロールへの IBOutlet接続は retain で(assign改め)
nib上のコントロールへの接続方法についての記事

UITableView の背景に画像を表示する

2010年7月9日金曜日 | Published in | 0 コメント

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

UITableView の背景に画像を表示したい。タイルパターンではなく1枚絵。こんなやつ。

※この絵は下記からダウンロードした。
Free Concrete Textures from TextureKing


UITableView.backgroundColor


まずネットで良く紹介されていたは UITableView.backgroundColor に画像を指定してみる。
self.tableView.backgroundColor =
    [UIColor colorWithPatternImage:[UIImage imageNamed:@"background.jpg"]];
self.tableView.backgroundColor = [UIColor clearColor];

結果はこう。
一見よさそうに見えるが、セルの両側にも背景画像が使われていてNG。
タイルパターン画像なら良いが、一枚絵の場合には使えない。

(結論)タイルパターンを背景画像に使いたい場合は backgroundColor が使える。


UIImageViewを下におく


他に良い方法が見つからなかったので、結局 UITableView の下に UIImageView を配置してそこへ画像を表示するようにした。InterfaceBuidlerで UIImageViewを配置し、あらかじめ背景画像を指定しておく。
UITableView の背景色は透明にしておく。これを忘れると背景画像が表示されない。
self.tableView.backgroundColor = [UIColor clearColor];

結果はこう。
plane の場合はこう。
スケスケになってしまった。この場合は UITableView.contentView.backgroundColor に色を指定しておく。
cell.contentView.backgroundColor = [UIColor whiteColor];

ステータスバー、ナビゲーションバー、ツールバーを半透明にする

2010年7月8日木曜日 | Published in | 4 コメント

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

各種バーの半透明化


プリインストールされている写真アプリなどは、ステータスバー、ナビゲーションバー、ツールバーが半透明になっている。
拡大図


※表示画像は下記から拝借した。
#530「ローテンブルクの乗用車」のフリー写真素材 (無料壁紙画像) - Futta.NET


コード


これを実現するコードはこんな感じ。UIViewController 内に記述する。
- (void)viewWillAppear:(BOOL)animated
{
 [super viewWillAppear:animated];

 // setup *bar translucent
 [UIApplication sharedApplication].statusBarStyle = UIStatusBarStyleBlackTranslucent;
 self.wantsFullScreenLayout = YES;    // ステータスバーの背景も描画対象にする

 self.navigationController.navigationBar.barStyle = UIBarStyleBlack;
 self.navigationController.navigationBar.translucent = YES;
 
 self.navigationController.toolbar.barStyle = UIBarStyleBlack;
 self.navigationController.toolbar.translucent = YES;

}

元に戻すコード
- (void)viewWillDisappear:(BOOL)animated {
 [super viewWillDisappear:animated];

 // restore *bar opacity
 [UIApplication sharedApplication].statusBarStyle = UIStatusBarStyleDefault;

 self.navigationController.navigationBar.barStyle = UIBarStyleDefault;
 self.navigationController.navigationBar.translucent = NO;

 self.navigationController.toolbar.barStyle = UIBarStyleDefault;
 self.navigationController.toolbar.translucent = NO;
}


参考情報


情報元は下記の本。この本は結構役に立つ。


「Unit 4.1 フルスクリーン」(P.120)

各バーがじわじわと透明になっていく(見えなくする)方法についても紹介されている。


 

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