分离实现更轻量的ViewControllers

ViewControllers 通常是 iOS 项目中最大的文件,并且它们包含了许多不必要的代码。所以 View controllers 中的代码几乎总是复用率最低的。我们将会看到给 viewControllers 瘦身的技术,让代码变得可以复用,以及把代码移动到更合适的地方。

把 Data Source 和其他 Protocols 分离出来

把 UITableViewDataSource 的代码提取出来放到一个单独的类中,是为 viewController 瘦身的强大技术之一。当你多做几次,你就能总结出一些模式,并且创建出可复用的类。

举个例,在示例项目中,有个 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;

}

这些代码基本都是围绕数组做一些事情,更针对地说,是围绕 viewController 所管理的 photos 数组做一些事情。我们可以尝试把数组相关的代码移到单独的类中。我们使用一个 block 来设置 cell,也可以用 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 个方法去掉了,取而代之,你可以创建一个 ArrayDataSource 类的实例作为 tableView 的 datasource。

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 映射到数组中的位置了,每次你想把这个数组显示到一个 tableView 中时,你都可以复用这些代码。你也可以实现一些额外的方法,比如 tableView:commitEditingStyle:forRowAtIndexPath:,在 tableViewControllers 之间共享。

这样的好处在于,你可以单独测试这个类,再也不用写第二遍。该原则同样适用于数组之外的其他对象。

在我们做的一个应用里面,大量使用了 CoreData。我们创建了相似的类,但和之前使用的数组不一样,它用一个 fetchedResultsController 来获取数据。它实现了所有动画更新、处理 section headers、删除操作等逻辑。你可以创建这个类的实例,然后赋予一个 fetchRequest 和用来设置 cell 的 block,剩下的它都会处理,不用你操心了。

此外,这种方法也可以扩展到其他 protocols 上面。最明显的一个就是 UICollectionViewDataSource。这给了你极大的灵活性;如果,在开发的某个时候,你想用 UICollectionView 代替 UITableView,你几乎不需要对 viewController 作任何修改。你甚至可以让你的 data source 同时支持这两个协议。

将业务逻辑移到 Model 中

下面是 viewController(来自其他项目)中的示例代码,用来查找一个用户的目前的优先事项的列表:

- (void)loadPriorities {

NSDate* now = [NSDate date];

NSString* formatString = @"startDate = %@";

NSPredicate* predicate = [NSPredicate predicateWithFormat:formatString, now, now];

NSSet* priorities = [self.user.priorities filteredSetUsingPredicate:predicate];

self.priorities = [priorities allObjects];

}

把这些代码移动到 User 类的 category 中会变得更加清晰,处理之后,在 ViewController.m 中看起来就是这样:

- (void)loadPriorities {

self.priorities = [user currentPriorities];

}

在 User+Extensions.m 中:

- (NSArray*)currentPriorities {

NSDate* now = [NSDate date];

NSString* formatString = @"startDate = %@";

NSPredicate* predicate = [NSPredicate predicateWithFormat:formatString, now, now];

return [[self.priorities filteredSetUsingPredicate:predicate] allObjects];

}

有些代码不能被轻松地移动到 model 对象中,但明显和 model 代码紧密联系,对于这种情况,我们可以使用一个 Store:

创建 Store 类

在我们第一版的示例程序的中,有些代码去加载文件并解析它。下面就是 viewController 中的代码:

- (void)readArchive {

NSBundle* bundle = [NSBundle bundleForClass:[self class]];

NSURL *archiveURL = [bundle URLForResource:@"photo data" 
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 对象来做这些事。通过分离,我们就可以复用这些代码,单独测试他们,并且让 view controller 保持小巧。Store 对象会关心数据加载、缓存和设置数据栈。它也经常被称为服务层或者仓库。

把网络请求逻辑移到 Model 层

和上面的主题相似:不要在 viewController 中做网络请求的逻辑。取而代之,你应该将它们封装到另一个类中。这样,你的 viewController 就可以在之后通过使用回调(比如一个 completion 的 block)来请求网络了。这样的好处是,缓存和错误控制也可以在这个类里面完成。

把 View 代码移到 View 层

不应该在 viewController 中构建复杂的 view 层次结构。你可以使用 Interface Builder 或者把 views 封装到一个 UIView 子类当中。例如,如果你要创建一个选择日期的控件,把它放到一个名为 DatePickerView 的类中会比把所有的事情都在 viewController 中做好好得多。再一次,这样增加了可复用性并保持了简单。

如果你喜欢 Interface Builder,你也可以在 Interface Builder 中做。有些人认为 IB 只能和 viewControllers 一起使用,但事实上你也可以加载单独的 nib 文件到自定义的 view 中。

关于通讯

其他在 viewControllers 中经常发生的事是与其他 viewControllers,model,和 views 之间进行通讯。这当然是 controller 应该做的,但我们还是希望以尽可能少的代码来完成它。

关于 viewControllers 和 model 对象之间的消息传递,已经有很多阐述得很好的技术(比如 KVO 和 fetchedResultsControllers)。但是 viewControllers 之间的消息传递稍微就不是那么清晰了。

当一个 viewController 想把某个状态传递给多个其他 viewControllers 时,就会出现这样的问题。较好的做法是把状态放到一个单独的对象里,然后把这个对象传递给其它 viewControllers,它们观察和修改这个状态。这样的好处是消息传递都在一个地方(被观察的对象)进行,而且我们也不用纠结嵌套的 delegate 回调。

此文章原文链接自己的个人博客: www.koalaliu.com ,因简书平台规范性以及用户量,搬至简书。

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

推荐阅读更多精彩内容