Help us understand the problem. What is going on with this article?

軽量なView Controller(objc.io #1-1 日本語訳)

More than 5 years have passed since last update.

※以下はobjc.io, Issue #1, Lighter View Controllersの日本語訳です。

軽量なView Controller

Issue #1 Lighter View Controllers, June 2013
By Chris Eidhof

view controllerはiOSプロジェクトの中で一番大きいファイルになりがちで、必要以上に多くのコードを含んでいることが多い。ほぼ決まってView Controllerはコードの中で最も再利用性の低い部分だ。View Controllerをスリムにし、再利用可能にして、より適切な場所にコードを移すテクニックを見ていこう。

この記事のサンプルプロジェクトがGitHubにあるので参照されたい。

データソースとその他のプロトコルを外に出す

View Controllerスリム化の最も強力なテクニックのひとつが、UITableViewDataSourceの部分を独立したクラスに移すことだ。これを2回以上やってみるとパターンが見えてきて、このための再利用可能なクラスを作ることになるだろう。

例えば我々のプロジェクトではPhotosViewControllerというクラスがあり、次のようなメソッドを持つ。

# pragma mark Pragma 

- (Photo*)photoAtIndexPath:(NSIndexPath*)indexPath {
    return photos[(NSUInteger)indexPath.row];
}

- (NSInteger)tableView:(UITableView*)tableView 
 numberOfRowsInSection:(NSInteger)section {
    return photos.count;
}

- (UITableViewCell*)tableView:(UITableView*)tableView 
        cellForRowAtIndexPath:(NSIndexPath*)indexPath {
    PhotoCell* cell = [tableView dequeueReusableCellWithIdentifier:PhotoCellIdentifier 
                                                      forIndexPath:indexPath];
    Photo* photo = [self photoAtIndexPath:indexPath];
    cell.label.text = photo.name;
    return cell;
}

このコードの多くの部分が配列に関するもので、一部がview controllerの管理するphotosに固有のコードだ。そこで配列に関するコードを独自クラスに移してみよう。ここではcellの設定にblockを使うが、ユースケースや好みに応じてdelegateにしても構わない。

@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

view controllerにあった3つのメソッドをなくし、代わりにこのオブジェクトのインスタンスを作ってtable viewのdata sourceとしてセットできる。

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;

これでindex pathを配列のインデックスにマッピングすることを考える必要がなくなり、table viewに配列を表示したい時はいつでもこのコードを再利用できる。さらに tableView:commitEditingStyle:forRowAtIndexPath:のようなメソッドを追加で実装して、このコードをすべてのtable view controllerで共有することも可能だ。

この方法の良いところは、このクラスを単体でテストでき、もう一度テストを書く必要がなくなることだ。もし配列ではないものを扱う場合であっても同じ原則が当てはまる。

今年我々が取り組んだアプリのひとつで、Core Dataをヘビーに使ったものがある。我々は同様のクラスを作ったが、配列を使うのではなく、fetched result controllerを使った。更新のアニメーションやsection header関連の処理、削除といったロジックをそこに実装した。このクラスのインスタンスを作ってfetch requestと、cellの設定のためのblockを与えれば、あとはこのクラスが面倒を見てくれる。

さらに、このアプローチは他のプロトコルにも応用できる。明らかな候補はUICollectionViewDataSourceだ。これによって非常に高い柔軟性が得られる。例えば開発中のある時点で、UITableViewの代わりにUICollectionViewを使うことになった場合、view controllerのコードはほとんど何も修正する必要がないだろう。data sourceに両方のプロトコルをサポートすることさえ、やろうと思えば可能だ。

ドメインロジックはモデルに

次の例は(別のプロジェクトの)view controllerのもので、ユーザーのactive priorityのリストを得るためのコードだ。

- (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];
}

これはしかし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];
}

コードによってはモデルオブジェクトに簡単に移動できないが、明らかにモデルのコードと密接に関連しているものがある。そんなときはストアクラスを利用することができる。

ストアクラスを作る

我々のサンプルアプリケーションの最初のバージョンにはファイルからデータを読み込んでパースするコードがあった。このコードは次のように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を小さく保つことができる。ストアはデータの読み込み、キャッシュ、データベーススタックの設定などを担当することができる。ストアはしばしば サービスレイヤー または レポジトリ とも呼ばれる。

ウェブサービスロジックはモデルレイヤーに

これは上記のトピックとよく似ている。ウェブサービスロジックをview controllerで処理してはならない。代わりにこれを別クラスにカプセル化しよう。view controllerはこのクラスのメソッドをコールバックハンドラ(例えばcompletionブロック)を使って呼ぶことができる。この方法の利点はキャッシングやエラーハンドリングもこのクラスの中でできることだ。

ビューのコードはビューレイヤーに

複雑なview階層の構築をview controllerの中で行うべきではない。Interface Builderを使うか独自のUIViewサブクラス内にカプセル化しよう。例えば独自のdate picker controlを作る場合、全てをview controllerの中で構築するよりDatePickerViewの中に入れる方がよい。これもまた再利用性とシンプルさを高めるテクニックだ。

もしInterface Builderを好むなら、この作業はInterface Builderを使っても可能だ。view controllerにしか使えないと思われがちだが、カスタムビューを個別のnibファイルから読み込むこともできる。サンプルアプリではphoto cellのレイアウトを含むPhotoCell.xibを作っている。

PhotoCell.xibスクリーンショット

上図のように、viewのプロパティを作って対応するsubviewにひもづけている(このxibではFile's Ownerは使用しない)。このテクニックは他のカスタムビューを作る際にも非常に便利だ。

コミュニケーション

その他にview controllerの中でよく起こることのひとつは、他のview controllerやモデル、ビューとのコミュニケーションだ。これこそview controllerがやるべきことなのだが、可能な限り少ないコードで済ませたい部分でもある。

view controllerとモデルオブジェクト間のコミュニケーションのためのテクニックにはよく知られた方法(KVOやfetched result controllerなど)があるが、view controller間のコミュニケーションに関してはそれほど明確ではないことが多い。

view controllerのひとつがある状態を持っていて、他の複数のview controllerとの間でやりとりをするような場合によく問題になる。この状態を別のオブジェクトに移して、そのオブジェクトを複数のview controller間で受け渡すようにするとうまく行くことが多い。情報がただ一カ所にあるため、入れ子になったdelegateコールバックに悩まされることがないという利点がある。これについては複雑なテーマなので、今後連載の1回分を費やすかもしれない。

まとめ

view controllerをより小さくするためのテクニックをいくつか見てきた。我々はこれらのテクニックを使えるところにはすべて使おうという努力はしない。我々のゴールはただひとつ、メンテ可能なコードを書くことだ。これまで見てきたパターンを知ることは、巨大で手に負えないview controllerをよりクリーンにする契機になるだろう。

参考リンク

gonsee
カレンダーシェアアプリ「TimeTree」のiOSアプリを開発しています。個人では「陣痛時計」、「ごみの日アラーム」というアプリをリリースしています。Perfumeエバンジェリスト。
https://simplebeep.net/iphone-apps
jubileeworks
「毎日に、新しい"なくてはならない"を創る」を経営理念に掲げ、共有カレンダーサービス TimeTree の開発・運営をしています。
https://timetreeapp.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away