[Objective-C] UITableView／UITableViewControllerについてのまとめ


色々ハマりどころや知らないことが多いので、やったことについてまとめていきます。

##UITableViewをアニメーションを使って更新する

Data Sourceを更新することで内容を更新することができますが、さらに更新された部分をアニメーションを持って更新できればなお分かりやすいですね。
そこで利用できるのが以下のメソッド。

* `UITableView#reloadSections:withRowAnimation:`
* `UITableView#reloadRowAtIndexPaths:withRowAnimation:`

ちょっとまだ動作の違いなどは確認中なのでメモ程度ですが、そもそもアニメーション用のメソッドが用意されているのはさすがな感じです。

ちなみに、現状表示されているセルを取得するには以下のメソッドを使うと`NSArray`に`NSIndexPath`が入った配列が返されるのでそれが利用できます。

```objc
// 現在表示されているセルのindexPathを取得
NSArray *indexPaths = [aTableView indexPathsForVisibleRows];
```


##UITableViewのデリゲートメソッド
UITableViewでは色々な設定をするときに、それぞれデリゲートメソッドを呼び出し設定していきます。（例えばセクションの数とか）
returnで適切に値を返せば、その値でテーブルビューがセットアップされます。
また、各種セルが選択されたときなどに呼び出されるデリゲートメソッドもあります。

| メソッド名 | 意味 |
|----------|------|
| - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView | テーブルに含まれるセクションの数を設定するとき |
| - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section | 該当セクションのRowの数を設定するとき |
| - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath | ひとつひとつのセルをセットアップするとき |
| - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath | セルひとつの高さを設定するとき |
| - (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section | セクションにヘッダーを設定するとき |
| - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section | ヘッダーの高さを設定するとき |
| tableView:didHighlightRowAtIndexPath: | セルがハイライトされたとき |
| tableView:didUnhighlightRowAtIndexPath: | セルのハイライトがキャンセルされたとき |
| - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath | セルが選択されたとき |


##NavigationControllerを使ってpush形式でビューを遷移させる

Storyboardを使わずにコードで実現する方法。手順としてはおそらく以下。

1. AppDelegateの`application:didFinishLaunchingWithOptions:`内で初期表示のViewControllerを生成。
2. 同様にUINavigationControllerを生成。
3. 2生成時、`initWithRootViewController:`メソッドでルートとなるViewControllerを指定（1で生成したもの）。
4. windowのsub viewとして、UINavigationControllerが持っているviewを`addSubview:`する。
5. windowの`makeKeyAndVisible`メソッドを呼び、レンダリングする。
6. 遷移時、遷移先のViewControllerを生成
7. 生成したViewControllreを`pushViewController:`メソッドに渡し、遷移させる。

実際のコード例は以下。

```objc
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    //windowを生成
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    
    //NavigationControllerをベースにしたビューを生成
    self.topViewController = [[TopViewController alloc] init];
    self.navigationController = [[UINavigationController alloc] initWithRootViewController:self.topViewController];
    
    [self.window addSubview:self.navigationController.view];
    
    //windowにsub viewをレンダリングするようメッセージ送信
    [self.window makeKeyAndVisible];
    
    return YES;
}
```

遷移させる場合のソースは以下。
（`self.navigationController`は、`initWithRootViewController`を指定した際に勝手に生える？）

```objc
- (void)doTransition
{
	DetailViewController *detailViewController = [[DetailViewController alloc] init];
    [self.navigationController pushViewController:detailViewController animated:YES];

}
```

##デリゲートメソッド内で選択されたセルを取得する
ハイライトや選択など、いくつかセルに対してのデリゲートメソッドが呼ばれることがある。
その際に、どのセルが選択されているのかを得るのが以下のコード。
indexPathオブジェクトはデリゲートメソッドの引数として予め渡されている。

```objc
// デリゲートメソッドを明示的に呼び出す
UITableViewCell *cell = [self tableView:tableView cellForAtIndexPath:indexPath];
```

##UITableViewCellの選択時の色をカスタマイズする

cell生成時にパラメータを設定する。パラメータは`cell.selectedBackgroundView`。

```objc
UITabileViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"ID"];

//background用のviewを生成
UIView *selectionColor = [UIView new];

//生成したviewのbackgroundColorに任意の色を設定
selectionColor.backgroundColor = [UIColor redColor];

//生成したbackgroundView用のviewを設定する
cell.selectedBackgroundView = selectionColor;

```

##UIRefreshControlを使って更新させる

Refresh用のコントロールが追加され、だいぶ簡単に「引っ張って更新」ができるようになったみたい。
使い方は以下。

1. UIRefreshControlを生成
2. 「引っ張って更新」を表示したいviewに対して`addSubview:`する
3. refreshControlにデリゲートを設定する
4. 更新処理が終わったらアニメーションを止める

「引っ張って更新」をさせたいViewに対して`addSubview:`すれば引っ張って更新の演出とともにそれようのviewが追加される。
完全に下まで引っ張ると設定したデリゲートメソッドが呼ばれるので、そこでなにがしかの更新処理を入れる。
更新が終わったことを伝えないとぐるぐるが出っぱなしになってしまうので、`[refreshControl endRefreshing]`として更新が終わったことを伝えれば、ぐるぐるが止まって元の状態に戻る。


##UISearchDisplayController, UISearchBarを使って検索をさせる

TableViewに`UISearchDisplayController`と`UISearchBar`を使って検索機能を持たせることができます。
だいぶハマったんですが、元々`UITableViewController`には`searchDisplayController`プロパティが設定されています。
ただ、コードのみで検索バーをどうやったら表示させられるのかが分かりませんでした。
色々な記事見ても、だいたいStoryboardを使って設定してしまっているので、どうやったらコードから表示させられるのか、delegateを正しく実行できるのかが分かっていませんでした。

が、どうやら`UISearchDisplayController`をインスタンス変数にして適切に設定することで動作するようになりました。
ざっくりと手順を書くと、

1. UISearchBarインスタンスを生成
2. UISearchDisplayControllerを、UISearchBarを伴ってインスタンス生成
3. UISearchDisplayControllerのdelegateにViewControllerを紐付け
4. `UISearchDisplayDelegate`、`UISearchBarDelegate`プロトコルのメソッドを実装
5. tableView.tableHeaderViewに、UISearchBarインスタンスを設定

という流れになります。

そして、デリゲートメソッドには検索中の文字列が渡されるので、それを元に絞り込みを行います。
ちなみに、UISearchDisplayControllerのviewはtableViewになっていて、検索対象のtableViewとセクションやセル生成の処理を共有することになります。
そのため、検索中かどうかでセルの見栄えを変える場合は処理を追加する必要があります。

###サンプルコード
実際に動いたコードを載せておきます。
（インスタンス変数に`searchDisplayController`を用意しておきます）

```objc
UISearchBar *searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, 44)];

searchDisplayController.delegate = self;
searchDisplayController.searchResultsDelegate   = self;
searchDisplayController.searchResultsDataSource = self;
self.tableView.tableHeaderView = searchBar;
```

```objc
// 検索時の実際の絞り込み処理
- (void)filterContentForSearchText:(NSString *)searchText scope:(NSString *)scope
{
    NSString *query = self.searchDisplayController.searchBar.text;
    if (query && query.length != 0) {
        NSPredicate *predicate = [NSPredicate predicateWithFormat:@"title CONTAINS[cd] %@", query];
        [self.resultsController.fetchRequest setPredicate:predicate];
        [NSFetchedResultsController deleteCacheWithName:@"cacheName"];
    }
    
    NSError *error = nil;
    if (![self.resultsController performFetch:&error]) {
        NSLog(@"Search error!");
    }
}

// 検索が開始された時点で呼ばれる
- (void)searchDisplayControllerWillBeginSearch:(UISearchDisplayController *)controller
{
    //
}

// 検索が実行された際に呼ばれる
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller
shouldReloadTableForSearchString:(NSString *)searchString
{
    [self filterContentForSearchText:[self.searchDisplayController.searchBar text]
                               scope:[[self.searchDisplayController.searchBar scopeButtonTitles] objectAtIndex:[self.searchDisplayController.searchBar selectedScopeButtonIndex]]];
    
    return YES;
}
```

これ以外にも、検索が終了した、などのデリゲートメソッドもあります。
とりあえずは上記だけで動くと思います。

なお、今回の例では`NSFetchedResultsController`を利用しているので、セクション数やセクション内のRowの数などは特に分岐せずにそのまま利用しています。
検索をさせる意味でも、NSFetchedResultsControllerは有用そうです。


### セルのセパレータの余白を0にする

iOS7から、UITableViewのセルのセパレータの左に若干の余白がつくようになりました。
これを設定するのが`separatorInset`です。
なので、以下のようにすると余白を消すことができます。

```objc:remove-cell-separator-padding
self.tableView.separatorInset = UIEdgeInsetsZero;
```


### 参考にした記事
* [UISearchDisplayController と NSFetchedResultContoller を組み合わせる](http://cocoadays.blogspot.jp/2010/07/uisearchdisplaycontroller_15.html)
* [Study CoreData 21 ~結果を絞り込め！~ (コクチもあるよ)](http://jacminik.wordpress.com/2010/07/30/study-coredata-21/)
* [UISearchDisplayController not working when created in code?](http://stackoverflow.com/questions/2395272/uisearchdisplaycontroller-not-working-when-created-in-code)