控制器“瘦身”大法

[翻译]本文翻译自objc.io创始人、iOS大神Chris Eidhof的文章, 原文链接可查看Lighter View Controllers

在iOS工程中,ViewController(界面控制器)通常是代码量最大的源码文件。它往往包含大量不必要的代码,而且当中的代码在绝大多数情况下都无法复用。今天我们来探究一些行之有效的方法,使得相关代码可以移动到更合适的地方,使其既能复用,控制器又能“瘦身”。关于本文内容的示例工程可以在GitHub下载。

分离数据源方法和其他协议方法

最强大的一项ViewController瘦身技术就是将UITableViewDataSource的代理方法转移到一个自定义类中。只要多次使用,你就会开始理解这个模式并创建相应的可复用类。
在本文的示例工程中, 有一个类叫PhotosViewController,它实现了以下方法:

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

以上大量代码和数组相关,而且其中某些代码还和ViewController管理的图片数组photos直接关联。我们尝试将和数组相关的代码转移到一个自定义类中去。我们使用一个block来配置cell,但出于你自身的使用情景和习惯,你也可以使用代理。

@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

现在控制器中原来的三个方法就可以移走了,取而代之的,你只需要创建这个对象的实例并将其设置为tableview的数据源。

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;

现在你不再需要担心如何将一个indexPath映射到array的具体某一个元素上了,而且每次你要在一个tableView中展示一个数组中的数据, 你都可以复用这段代码。你还可以实现其他的DataSource方法使其在你所有的tableView控制器中可以共用,例如 tableView:commitEditingStyle:forRowAtIndexPath:方法。

这样做还有一个好处,就是我们可以单独 测试这个类,而且再也不用担心需要重写一遍。除了数组以外的其他数据源,也适用同样的原则。

在今年的一个公司项目中,我们使用了大量的CoreData。我们创建了一个类似的类,但它的数据源不是一个数组,而是一个结果控制器。它实现了动画更新、section头部配置、删除的所有逻辑。有了这个类后,你只需要在控制器中创建一个它的实例对象,并赋值一个网络请求和block来配置cell,之后的逻辑将全部由它负责完成。

这种方法还可以延伸到其他协议中,一个典型的例子就是UICollectionViewDataSource。这就给你提供了巨大的灵活性,在你开发的某个阶段,当你决定使用UICollectionView来替代原先的UITableView时, 你几乎不需要改动ViewController中的任何代码。你甚至还可以让你的dataSource类同时支持两种协议。

转移“领域逻辑”到实体(Model)层

以下这段代码写在一个ViewController中(来自其他工程),用于筛选出User类中活跃列表。

- (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就会显得简洁很多,如下所示:

- (void)loadPriorities {
  self.priorities = [self.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];
}

有些代码和实体的关联性强,但又无法简单地转移到一个Model类中,这时我们可以使用一个存储(Store)类。

创建存储(Store)类

在示例工程的第一版中有一些代码是关于从文件中加载并解析数据的。这段代码被写在了ViewController中:

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

ViewController其实并不需要知道这段代码的逻辑。我们此时就可以创建一个Store对象来实现它。通过把这段代码分离出去,我们可以重复使用它、可以单独测试它、还能令我们的ViewController体量更小。这个Store类可以负责数据的加载、缓存,以及创建数据库。这个Store类通常也叫做service层或者仓库(repository)。

转移网络服务逻辑到实体(Model)层

这一点和上一点很类似:即不要在你的ViewController中实现网络服务逻辑。你应该将其封装到一个不同的类中,之后你的ViewController就可以调用这个类中的网络方法,并通过一个回调执行之后的逻辑(例如一个叫completion的block)。这样做的好处是你可以在这个类中实现所有的缓存工作和错误处理。

转移视图代码到视图(View)层

ViewController中不应该存在复杂结构的视图(Views)构建代码。要么你就用Interface builder,要么你就把这个View封装起来。举个例子,如果你要构建一个自定义的日期选择器,比起将代码写到ViewController中, 还不如写到一个叫DatePickerView的类中更合理。而且该做法令代码更容易使用、也更好重复使用。

如果你喜欢使用Interface Builder,你也可以在Interface Builder中实现这段逻辑。有些人会以为这种方法只能应用到ViewController中,但实际上你也可以通过独立的nib文件加载自定义Views。在我们的示例app中,我们创建了一个 PhotoCell.xib文件,它包含了photoCell的完整布局。

PhotoCell.xib screenshot

正如你所见的,我们在这个View上创建了两个label标签,并将其关联到一个的类中(在这个xib中我们并不使用File’s Owner对象)。这项技术对于其他自定义视图来说也相当容易上手。

控制器通信问题

ViewController中,我们经常要处理它和其他ViewControllermodelview之间的通信问题。这虽然正是一个控制器应该做的事,但我们也希望用尽可能少的代码实现它。

视图控制器和模型对象之间的通信有很多成熟的技术(例如KVO和结果控制器),但是视图控制器之间的通信代码往往不太清晰。

我们经常遇到这样的问题:一个视图控制器具有某种状态并与多个其他视图控制器通信。 通常情况下,将这个状态放入一个单独的对象并传递给其他视图控制器是合理的,然后其他视图控制器就可以观察并修改该状态。 这样做的好处是这个状态放在同一个地方,不至于最终让这段逻辑在嵌套的代理或者回调中“苦苦纠缠”。 但这是一个复杂的问题,未来我们可能提出一个课题专门研究它。

总结

我们已经学到了一些创建较小视图控制器的技巧。 但我们不应该为了使用这些技巧而使用,因为使用这些技术只有一个目标:编写可维护的代码。 通过了解这些模式,我们才更有可能用清晰的代码结构去编写复杂的视图控制器。

延伸阅读

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352

推荐阅读更多精彩内容