まーぽんって誰がつけたの?

iOS→Scala→インフラなおじさん技術メモ

ViewCotrollerを軽くする

これはすごくいいこと書いてありそうだ!と思ってお気に入りした 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を作成しています。

f:id:masato47744:20140527025048p:plain

このスクリーンショットのように、カスタムサブビュークラス(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をクリアにするよりよいチャンスを得ることができるでしょう。