简化TableView之路一

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

你可以在 Github 上获取关于这个问题的示例项目

把 Data Source 和其他 Protocols 分离出来

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

举个例,在示例项目中,有个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 数组做一些事情。我们可以尝试把数组相关的代码移到单独的类中。我们使用一个 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类的实例作为 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 controllers 之间共享。

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

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

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

将业务逻辑移到 Model 中

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

- (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 中会变得更加清晰,处理之后,在View Controller.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 类

在我们第一版的示例程序的中,有些代码去加载文件并解析它。下面就是 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 没必要知道这些,所以我们创建了一个Store对象来做这些事。通过分离,我们就可以复用这些代码,单独测试他们,并且让 view controller 保持小巧。Store 对象会关心数据加载、缓存和设置数据栈。它也经常被称为服务层或者仓库

把网络请求逻辑移到 Model 层

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

把 View 代码移到 View 层

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

如果你喜欢 Interface Builder,你也可以在 Interface Builder 中做。有些人认为 IB 只能和 view controllers 一起使用,但事实上你也可以加载单独的 nib 文件到自定义的 view 中。在示例程序中,我们创建了一个PhotoCell.xib,包含了 photo cell 的布局:

就像你看到的那样,我们在 view(我们没有在这个 nib 上使用 File's Owner 对象)上面创建了 properties,然后连接到指定的 subviews。这种技术同样适用于其他自定义的 views。

通讯

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

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

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

总结

我们已经看到一些用来创建更小巧的 view controllers 的技术。我们并不是想把这些技术应用到每一个可能的角落,只是我们有一个目标:写可维护的代码。知道这些模式后,我们就更有可能把那些笨重的 view controllers 变得更整洁。

扩展阅读

View Controller Programming Guide for iOS

Cocoa Core Competencies: Controller Object

Writing high quality view controllers

Stack Overflow: Model View Controller Store

Unburdened View Controllers

Stack Overflow: How to avoid big and clumsyUITableViewControllerson iOS


目前代码做的只适合简单的tableView布局,在后续几天内会陆续完成封装。此处转载objc.io的文章特此声明,参考了很多关于简化的思路和代码,Three20,还有一些网友写的。特此感谢。

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

推荐阅读更多精彩内容