ViewCotrollerを軽くする
objc.io何がいいかと言うと、数日間APIをいじくり回してやっとわかる使い方のコツとか内部的な動きがサラっと書いてたり、実装のアプローチを複数紹介してたり、実際にアプリにいかすとこにトコトンフォーカスしてて、なんかスゲー編集力を感じる。
— Taku Okawa (@t4ku) 2014, 5月 11
これはすごくいいこと書いてありそうだ!と思ってお気に入りした objc.io
読もうと思ってもついついtwiiterとかはてブとか見て時間が過ぎて行っていく。。
なので、日本語にしてブログに書くことで自分にプレッシャーを与えよう。そうしよう。
まずは、第一回の1つめ、#1 Lighter view controllersの意訳してみた。流し読みするぐらいだと気づかないけど、意外と分からない英語あって、Google翻訳さんを見ながら想像で書いてしまったところもある。真面目に訳そうと思うと、weとかyouを訳すと自然な日本語にならないのと、カンマでつながれた文がどこにかかってるのかがよく分からなくて、その辺難しいなと思った。
それではスタート
ViewControllerを軽くする
iOSプロジェクトでは、view controllerはすぐに巨大なファイルになってしまいます。そして、それほど、重要じゃないコードもたくさん含まれています。なので、たいていview controllerは、再利用できないコードになってしまいます。そんなview controllerを軽量にして、コードを再利用可能にし、ふさわしい場所にコードを移すテクニックを紹介しましょう。
DataSourceやその他のプロトコルを分離する
view controllerを軽くする一番効果的な方法は、UITableViewDataSource
を別の自作クラスに移行することです。きっと、あなたも何度もこの方法を繰り返してるうちに、再利用可能なクラスのパターンが分かってくると思います。
まずは、このサンプルプロジェクトのPhotosViewController
を見て下さい。
たくさんの配列操作をするコードがあり、そのうちのいくつかは、PhotosViewController
が管理する固有のphotos
を操作しています。
/// photoを取得する - (Photo*)photoAtIndexPath:(NSIndexPath*)indexPath { // ダメポイント1: view controllerのインスタンス変数っぽいphotosを使っている return photos[(NSUInteger)indexPath.row]; } // UITableViewDataSouceプロトコルのメソッド(行数を返す) - (NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section { // ダメポイント2: view controllerのインスタンス変数っぽいphotosを使っている return photos.count; } // UITableViewDataSouceプロトコルのメソッド(セルを表示) - (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath { PhotoCell* cell = [tableView dequeueReusableCellWithIdentifier:PhotoCellIdentifier forIndexPath:indexPath]; // ダメポイント3: 固有のクラスPhotoが前提になっている Photo* photo = [self photoAtIndexPath:indexPath]; cell.label.text = photo.name; return cell; }
それでは、早速、配列に関するコードを自作クラスにうつしてみましょう。 今回は、セルの設定にblockを使ってますが、別にdelegateでもよくて自分のユースケースや好みでOKです。
/// 自作のDataSourceクラス /// itemsという汎用的なデータを持っているので、どんな配列でも受け取ることができる @implementation ArrayDataSource - (id)itemAtIndexPath:(NSIndexPath*)indexPath { return items[(NSUInteger)indexPath.row]; } - (NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section { return items.count; } - (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath { id cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath]; id item = [self itemAtIndexPath:indexPath]; configureCellBlock(cell,item); return cell; } @end
上の3つのメソッドはview controllerから取り除くことができて、かわりに以下のように、ArrayDataSource
のインスタンスを作成して、tableViewのdata sourceに指定します。
- (void)setupTableView { void (^configureCell)(PhotoCell*, Photo*) = ^(PhotoCell* cell, Photo* photo) { cell.label.text = photo.name; }; photosArrayDataSource = [[ArrayDataSource alloc] initWithItems:photos cellIdentifier:PhotoCellIdentifier configureCellBlock:configureCell]; self.tableView.dataSource = photosArrayDataSource; }
これで、再利用可能なtableViewを使って、配列を表示しようとするときに、配列とindexPathのマッピングを心配する必要はなくなりました。あと、tableView:commitEditingStyle:forRowAtIndexPath:
のようなdataSourceプロトコルも全てのTableViewControllerで共有することができます。
この方法のいいところは、このArrayDataSource
クラスを単体でテストすることができるし、テストを再度書く必要がなくなります。他の配列を使った場合でも同じ原理で動くからです。
私たちが今年開発していたアプリケーションの中にCoreDataを多用していたものがありました。
そこでも、同じようなクラスを作成しましたが、配列に連動させていたのはUITableViewController
ではなく、NSFetchedResultController
でした。
その自作クラスでは、アップデートの際のアニメーションやセクションヘッダーの表示、行の削除など全てのロジックを含んでいます。あとは、自作クラスのインスタンスを作成し、NSFetchedResultController
に渡して、blockでセルの設定などをすれば、あとはそのインスタンスが残りの全ての処理を行ってくれます。
さらに、このアプローチは他のプロトコルに対しても適用できます。
例えば、すぐに思い浮かぶ候補といえば、UICollectionViewDataSource
です。
このアプローチは非常に高い柔軟性を得る事ができます。もし、開発中に、UITableView
のかわりに、UICollectionView
を使おうと思ったときに、ほとんど何もview controllerのコードを変更せずにすみます。なんなら、両方のdata sourceプロトコルに適合するview controllerでさえ作ることもできます。
ビジネスロジックはモデルに移す
ここで、ユーザーの優先事項リストを検索することになっているview controller(さっきとは別プロジェクト)の例を見て下さい。
- (void)loadPriorities { NSDate* now = [NSDate date]; NSString* formatString = @"startDate <= %@ AND endDate >= %@"; NSPredicate* predicate = [NSPredicate predicateWithFormat:formatString, now, now]; NSSet* priorities = [self.user.priorities filteredSetUsingPredicate:predicate]; self.priorities = [priorities allObjects]; }
しかし、このコードはViewControllerに実装するよりも、Userクラスにカテゴリ実装した方がよりすっきりしますよね。以下のような感じです。
- ViewController.m
- (void)loadPriorities { self.priorities = [user currentPriorities]; }
- User+Extensions.m
- (NSArray*)currentPriorities { NSDate* now = [NSDate date]; NSString* formatString = @"startDate <= %@ AND endDate >= %@"; NSPredicate* predicate = [NSPredicate predicateWithFormat:formatString, now, now]; return [[self.priorities filteredSetUsingPredicate:predicate] allObjects]; }
一方で、簡単にモデルオブジェクトに移行できそうにないが、明らかにモデル的な処理の場合があると思います。 そういう場合はStore(保存用)クラスを使います。
保存用のクラスを作る
私たちが最初に書いたサンプルアプリケーションでは、ファイルからデータを読み込む処理をview controllerに実装していました。以下のメソッドです。
- (void)readArchive { NSBundle* bundle = [NSBundle bundleForClass:[self class]]; NSURL *archiveURL = [bundle URLForResource:@"photodata" withExtension:@"bin"]; NSAssert(archiveURL != nil, @"Unable to find archive in bundle."); NSData *data = [NSData dataWithContentsOfURL:archiveURL options:0 error:NULL]; NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data]; _users = [unarchiver decodeObjectOfClass:[NSArray class] forKey:@"users"]; _photos = [unarchiver decodeObjectOfClass:[NSArray class] forKey:@"photos"]; [unarchiver finishDecoding]; }
本当は、view controllerは、このような処理を担当するべきではありません。このような処理を担当するための保存用のオブジェクトを作成するべきです。 処理を分離することによって、保存用のコードを再利用することができて、かつ、別々にテストすることができます。そして、view controllerを小さく保っておくことができます。 この保存用のオブジェクトで、データのロード、キャッシング、および、データベースに関する処理を行うことができます。このような形式のクラスは一般的にサービス層、または、リポジトリと呼ばれています。
Webのデータ通信ロジックはモデルに移す
これは、さっきのStoreクラスを作るという上のトピックと非常に近いものがありますが、view controller内に、Webのデータ通信ロジックを実装するなという話です。 その代わりに、別のクラスでWebのデータ通信ロジックをカプセル化します。view controllerは、コールバック(例えば、completion blockみたいに)を使って通信メソッドを呼び出します。このアプローチのいいところは、データのキャッシングやエラーハンドリングを、全てWebのデータ通信用のクラスで行えるようになることです。
Viewに関するコードはView層へ移す
複雑なビュー階層の構築はview controllerでやるべきではなく、InterfaceBuilderを使ったり、UIViewをサブクラス化して処理をカプセル化するべきです。例えば、独自のdate pickerを作りたい場合、view controllerで全てを作るよりはDataPickerViewクラスに詰め込んだ方が理にかなっています。 大事なことなので二度言いますが、これは再利用性とシンプルさを向上させます。
Interface Builder好きなら、もちろんInterface Builderでもできます。 もしかしたら、Interface Builderは、view controllerでしか使えないと思う人もいるかもしれませんが、実は、カスタムビュー用に別のnibファイルを読み込むこともできます。 今回のサンプルアプリケーションでは、photo cellのレイアウトが含まれているPhotoCell.xibを作成しています。
このスクリーンショットのように、カスタムサブビュークラス(File's ownerは使ってません)にIBOutletで接続するViewのプロパティを作成しました。このようなxibを用いてカスタムクラスに詰め込むやり方は、どんなカスタムビューでも役に立ちます。
オブジェクト同士のメッセージのやりとり
view controllerでよく発生することは、他のview controller、モデル、viewとのやり取りです。 これこそまさにcontrollerの役割なわけですが、それでも、なんとかして最小限のコードにまとめたいわけです。
view controllerとmodel間のやりとりで、とてもよく考えられている方法(例えばKVOやNSFetchedResultsControllerなど)はたくさんあります。しかし、view controller間のやりとりについては、あまり明確な方法はありません。
あるview controllerが複数の状態を持っていて、複数のview controllerとやり取りをするときに、私たちはよく困難に出会います。こういった場合には、複数の状態をまとめたオブジェクトについて状態をチェックし修正して、view controller間で渡す方法は理にかなったよい方法です。これの利点は、全てを1カ所で管理することができて、ネストされたdelegateのcallbackに陥ることがなくなります。これは複雑な問題であり、いつか、issue全体を専用にするかもしれない。
結論
このissueを通して、より軽量なview controllerを作成するためのいくつかのテクニックを見てきました。 しかし、保守しやすいコードを書くという唯一つの目標のために、なんとかしてこれらの技術を使おうとする努力はしません。これらのパターンを知ることによって、扱いにくいview controllerをクリアにするよりよいチャンスを得ることができるでしょう。