前言
可能之前的表述不是特别明确,我的方案不是静态页面的通用实现。我的方案针对的是类似一些设置界面之类的简单的静态的tableView。看到有很多人认为这样的方案感觉实现起来会变麻烦,这个可能就是思考问题的侧重点不同。我思考的侧重点是后期的维护修改、应对频繁的需求变更 欢迎交流~~
静态的tableView
类似设置界面、个人主页等等几乎是每个APP都会涉及到的一个模块。我相信大家都有一些自己的套路来如何处理这类界面。写这篇文章的目的是抛砖引玉想要和大家来交流交流。
一些常见的写法
从具体的写法来切入,以下是我能想到的一些写法。
1. 山顶洞人写法
啥都不封装
if (indexPath.section == 0) {
if (indexPath.row == 0) {
}else if (indexPath.row == 1) {
}
}else if (indexPath.section == 1) {
if (indexPath.row == 0) {
}else if (indexPath.row == 1) {
}
}
复制代码
各种嵌套if
判断indexPath.section
indexPath.row
拿到对应的cell显示或者跳转。这种方式可读性差,不好扩展应该没人这么写了吧,可能你刚学iOS开发的时候这么写过。
2. 纯代码 + 枚举
用一条枚举来对应的一条cell。 数据源用枚举数组,或者也可以用带有枚举属性的对象数组。
self.dataArray = @[@[@(settingTypeAccount)],
@[@(settingTypeMessage),@(settingTypePrivacy)],
@[@(settingTypeHelp),@(settingTypeAboutUs)]];
复制代码
代理方法里可以拿到枚举直接用switch判断
switch (type) {
case settingTypeHelp:
break;
.
.
.
default:
break;
}
复制代码
用switch来判断具体具体的cell,首先可读性相较于if判断高了不少,并且在增加删除cell的情况下,xcode会有提示来帮助不至于漏掉一些地方。这种方式会有比较多的重复代码,而且在添加、删除、调整cell的时候不够高效。
3. storyboard的静态cell
首先storyboard
方式相较于纯代码,不用跑起来就能看见界面,相对比较直观。而且在开发速度方面也有不小的优势。但是我从自身开发过程中的情况看来,这种方式在需求频繁变更的情况下还是比较蛋疼的。
4. 加一层中间层
没有什么封装是加一层中间层解决不了的,如果有那么再加一层 -- 鲁迅
?开个玩笑,来看看加一层怎么样操作。首先我认为要有一个概念,封装在一定程度上是不会减少代码量的。该写的代码你还是要写的,只是合理的结构可以让代码可读性更好,可扩展性也更好。
@interface tableModel : NSObject
- (void)addASection:(tableSectionModel *)section;
@property (nonatomic,strong) NSMutableArray <tableSectionModel *> *sections;
.
.
.
@end
@interface tableSectionModel : NSObject
- (void)addARow:(tableRowModel *)row;
@property (nonatomic,strong) NSMutableArray <tableRowModel *> *rows;
.
.
.
@end
@interface tableRowModel : NSObject
@property (nonatomic,assign) NSInteger rowHeight;
.
.
.
@end
复制代码
我们一开始就已近知道了table是如何展示的,包括cell的显示顺序,cell的显示样式、行高等等。那么我们可以把能够描述一个cell的所有的数据都抽象成一个rowModel的数据,然后把能描述每个section的的所有数据抽象成一个sectionModel的数据。那么我们只需要生成对应的sectionModel的数组就可以来描述一个table了,然后我们在数据源里解析model里面的数据完成显示。这种方式已经相对比较合理了,但是内部还是有比较大的封装余地。
show you my code
我看过挺多的类似三行代码实现设置界面
的方案,基本都是上面第四种方法的进一步封装,内部实现了几种常见的cell样式,用一个枚举来对应具体的样式,然后给每个每个rowModel添加一个cell样式的属性。这样一来通过简单的设置rowModel的cell样式就能拿到具体的cell。当然这样的方式已经能够应对大部分的情况,并且写起界面来也是很爽了。但是我还是有几个地方不是太满意,需要去尝试解决这些不满意。
1. 添加section和row的方式
我希望添加section和row的时候这部分的代码是一个整体,单纯- (void)addASection:(tableSectionModel *)section;
这样的写法在我看来还不够整体,因为你没办法保证这部分的代码一定是写在了一个地方。思来想去,我想到了Masonry
的写法。
[self.view mas_makeConstraints:^(MASConstraintMaker *make) {
// do something
}];
复制代码
这种写法解决了我不满意的地方,所以最后我希望的写法是
[self.tableView zhn_addSection:^(ZHNStaticTableSection *section) {
[section zhn_addRow:^(ZHNStaticTableRow *row) {
}];
.
.
.
}];
复制代码
2.dataSource
delegate
重复代码的问题
我们清楚dataSource
和delegate
是一对一的,所以代理方法和数据源方法肯定是只能写在一个地方。你可能会说那么我们再加一层Manger来管理sectionModel的数组,然后把tableView
的数据源和代理设置为manager,然后在manager内部实现dataSource
和delegate
解析sectionModel的数组展示界面。这样一来我们只需要配置sectionModel就可以了。但是这样做那么万一我们在控制器上想要监听tableView
的滑动呢?思来想去,我最后尝试用消息转发 + 断言
的方式来尝试解决这个问题。
消息转发实现代理一对多
代理是一对一的,通知是一对多的。
刚开始学iOS的时候,我们肯定都听过这样一句话,来描述代理和通知的不同。但是其实通过消息转发,我们也是可以来实现代理的一对多的。
如果对消息转发没啥概念的可以看看这篇博客。简单理解就是当调用方法的时候,系统通过isa指针层层查找方法列表,找不到方法的时候,在报找不到方法之前,系统还额外提供了几个方法提供给我们去实现这个方法。
代理一对多的主要实现的逻辑:
-
1.提供一个delegate容器,存放代理。
-
2.
- (BOOL)respondsToSelector:(SEL)aSelector
判断代理容器里的代理如果实现了代理方法,这个方法需要返回YES。如果返回NO,系统就判定没有实现代理方法,那么就不会调用方法,那么也就不会有后面的一系列的流程了。 -
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
返回方法的签名。不返回签名后面的消息转发方法也不会调用。
-
- (void)forwardInvocation:(NSInvocation *)anInvocation
方法里遍历delegate容器,转发方法。
断言
断言 (assertion) 在 Cocoa 开发里一般用来在检查输入参数是否满足一定条件,并对其进行“论断”。这是一个编码世界中的哲学问题,我们代码的使用者 (有可能是别的程序员,也有可能是未来的自己) 很难做到在不知道实现细节的情况下去对自己的输入进行限制。大多数时候编译器可以帮助我们进行输入类型的检查,但是如果代码需要在特定的输入条件下才能正确运行的话,这种更细致的条件就难以控制了。在超过边界条件的输入的情况下,我们的代码可能无法正确工作,这就需要我们在代码实现中进行一些额外工作。
上面这段介绍是从喵神一篇断言tips里的摘抄。在很多的第三方库中你肯定也见过类似比如AFNetworking
中随便一搜NSAssert(NO, @"State method should never be called in the actual dummy class");
类似的断言非常常见。简单理解当我们输入一个不合法参数的情况的时候,程序就直接崩溃了,并且打印了断言里的描述。那么我们一眼就能知道我们的输入出问题了,并且问题出在哪里。
这里我们为什么用断言,由于我内部实现了某些数据源和代理。那么我们肯定不希望外部再实现这些实现过的方法。那么我们肯定需要做一些约束,这里用断言显然是最合适的。如果外部实现了实现过的方法,直接崩溃并且打印提示信息。
3. 方便切换
类似三行代码实现设置界面
的方案内部定义几种样式的方案,如果我们想要把我们已经写好的界面切换到这种方案下,代价相对还是比较大的。我们项目中肯定也实现了一些cell,我希望我之前的cell能无缝的接入进去。针对这种情况我在rowModel里添加了一个cellClass属性来指定cell,和一个displayCellHandle
block
来设置cell的一些样式。
瞄一眼写法
[self.tableView zhn_initializeEnvironmentWithDefaultRowHeight:44
defaultCellClass:[NormalSettingTableViewCell class]
defaultSectionHeader:nil
defaultHeaderHeight:20
defaultSectionFooter:nil
defaultFooterHeight:0
originalDelegate:self
originalDatasource:self];
[self.tableView zhn_addSection:^(ZHNStaticTableSection *section) {
[section zhn_addRow:^(ZHNStaticTableRow *row) {
row.displayCellHandle = ^(UITableView *tableView, UITableViewCell *cell, NSIndexPath *indexPath) {
cell.textLabel.text = @"账号与安全";
};
row.selectCellHandle = ^(UITableView *tableView, NSIndexPath *indexPath) {
NSLog(@"账户与安全");
};
}];
}];
[self.tableView zhn_addSection:^(ZHNStaticTableSection *section) {
[section zhn_addRow:^(ZHNStaticTableRow *row) {
row.displayCellHandle = ^(UITableView *tableView, UITableViewCell *cell, NSIndexPath *indexPath) {
cell.textLabel.text = @"新消息通知";
};
}];
[section zhn_addRow:^(ZHNStaticTableRow *row) {
row.displayCellHandle = ^(UITableView *tableView, UITableViewCell *cell, NSIndexPath *indexPath) {
cell.textLabel.text = @"隐私";
};
}];
[section zhn_addRow:^(ZHNStaticTableRow *row) {
row.displayCellHandle = ^(UITableView *tableView, UITableViewCell *cell, NSIndexPath *indexPath) {
cell.textLabel.text = @"通用";
};
}];
}];
复制代码
总结
代码在这里 github.com/zhnnnnn/ZHN…
这是我的方案,还没来得及在实际的项目中使用过。抛砖引玉,希望大家能够不吝赐教。我也很想知道大型知名项目里大家都是怎么写这部分代码的。