TableViewのコードをクリーンにまとめる

Clean table view code - Lighter View Controllers - objc.io issue #1 の訳です。

TableViewはiOSアプリにおいて色々なことができるパーツです。 それゆえ、直接的、あるいは、間接的なテーブルビューのタスクに関するコードが多くなってしまいます。例えば、データを供給したり、テーブルビューを更新したり、ふるまいを制御したり、選択したときの制御をしたりなどです。そこで、この記事では、table viewをきれいに構造化し、クリーンな状態にキープする方法を紹介します。

UITableViewControllerUIViewControllerの比較

AppleTableView専用のview controllerクラスとして、UITableViewControllerを提供しています。table view controllerは、何度も定型的な同じコードを書かなくて済むように、とても役に立つ機能を少しだけ実装しています。

その反面、table view controllerは画面いっぱいに表示されるただ一つのtable viewしか管理できません。まぁでもたいていの場合、table view controllerのその制約があったとしても、事足りてしまう訳ですが、それ以外のケースもあると思います。そのケースについても、ちゃんと動作する例をいくつかこれから書いていきます。

Table View Controllerの機能

table view controllerはtable viewの初回表示時にデータを読み込んでくれます。 もう少し細かく説明すると、キーボードのイベント通知に反応して、table viewの編集モードを切り替えてくれたり、些細なことですが、スクロールインジゲータを点滅させたり、選択状態をクリアしてくれたりもします。カスタムのサブクラスにしたときは、これらの機能を正しく動作させるためには、view controllerの各ライフサイクルイベント(viewWillAppear:viewDidAppear:など)をオーバライドするときには、[super viewWillAppear:animated][super viewDidAppear:animated]を必ず呼ぶ必要があります。

table view controllerは、「引っ張って更新」というAppleが実装している標準のview controllerにはないセールスポイントを持っています。現時点では、table view controllerにおいて、このUIRefreshControlが、唯一の文書化された方法です。他のコンテキストで動作させるための方法がありますが、これらは簡単に次のiOSのアップデートでは動作しなくなるかもしれません。

これらすべての要素で、Appleが定義している標準的なtable viewのUI動作の多くを提供します。 アプリがこれらの規格に準拠している場合は、定型的なコードを書くことを避けるために、テーブルビューコントローラに従うことをお勧めします。

Table View Controllerの限界

table view controllerは(view controllerが必ず持っている)viewプロパティにtable viewを設定しています。もしあなたがtable view controllerを採用したあとで、画面上のtable viewのそばに別のビュー(例えば地図とか)を表示したいと思ったときに、無理矢理なハックをしたくないという思いがあるなら、残念ながら標準のview controllerに移行することなしにそれを実現するのは難しいことです。

あなたがコードやxibファイルを使用して、UIを定義していた場合、標準のview controllerに移行することはとても簡単です。 ただ、storyboardを使用している場合は、移行にはより多くの手順が必要です。storyboardを使用している場合、table view controllerを標準のview controllerを作成するためには、もう一度作り直しが必要です。これはどういうことかというと、もう一度新しいview controllerとワイヤにすべての内容を上書きコピーする必要があるということです。

さらには、最後に、この移行によって失われたtable view controllerの機能を再度追加する必要があります。 それらのほとんどは、viewWillAppear:viewDidAppear:に実装する単純な単一行の文です。 編集状態をトグルすることはtable viewの編集プロパティを反転させるアクションメソッドの実装を必要とするということです。ほとんどの作業はキーボードのサポートを再作成することになります。

あなたががこの道におちいる前に、この懸念を取り去るさらなるメリットを持った簡単な代替手段があります。

Child View Controllers

全てのtable view controllerを取り除く代わりに、table view controllerをchild view controllerとして、別のview controllerに追加することもできます。(このissueのview controller containment参照)table view controllerは引き続き一つのtable viewを管理し、親のview controllerは、table view以外のあなたが必要とする全てのUI要素を担当することができます。

- (void)addPhotoDetailsTableView
{
    DetailsViewController *details = [[DetailsViewController alloc] init];
    details.photo = self.photo;
    details.delegate = self;
    [self addChildViewController:details];
    CGRect frame = self.view.bounds;
    frame.origin.y = 110;
    details.view.frame = frame;
    [self.view addSubview:details.view];    
    [details didMoveToParentViewController:self];
}

もし、この解決方法を使うなら、child view controllerと親view controllerがやり取りするためのチャネルを作ってあげる必要があります。たとえば、もし、ユーザーがtable viewのセルを選択したら、親view controllerは別のview controllerをpushするために、選択したということを知る必要があります。このユースケースの場合なら、一番きれいなやり方は、table view controllerにdelegate protocolを用意してあげることです。そして、親view controllerはそのdelegate protocolを実装します。

@protocol DetailsViewControllerDelegate
- (void)didSelectPhotoAttributeWithKey:(NSString *)key;
@end

@interface PhotoViewController () <DetailsViewControllerDelegate>
@end

@implementation PhotoViewController
// ...
- (void)didSelectPhotoAttributeWithKey:(NSString *)key
{
    DetailViewController *controller = [[DetailViewController alloc] init];
    controller.key = key;
    [self.navigationController pushViewController:controller animated:YES];
}
@end

サンプルコードを見て分かると思いますが、この構造は、view controller間のやりとりに多少のオーバヘッドはありますが、その代わり、table viewに関する部分をきれいに分離することができています。特定のユースケースによっては、より単純なものを必要以上に複雑にしてしまうこともあります。どうするかはよく検討して決めてください。

関心ごとの分離

table viewは、モデル、ビュー、コントローラの境界を越えるような様々なタスクを扱うことになります。これら全てのタスクを配置するの場所がview controllerになってしまうのを防ぐために、私たちは、できる限りより適正な場所にこれらのタスクの多くを分離しようとしています。これは、可読性、保守性をテスタビリティを上げるのに役立ちます。