|
| 1 | +> 本文是直播分享的简单文字整理,视频地址:[优酷](http://v.youku.com/v_show/id_XMTUzNzQzMDU0NA==.html)、[YouTube](https://youtu.be/hPR67T9mbsY) |
| 2 | +> Demo 地址:[KtTableView](https://github.com/bestswifter/MySampleCode/tree/master/KtTableView) |
| 3 | +
|
| 4 | +# MVC |
| 5 | + |
| 6 | +讨论解耦之前,我们要弄明白 MVC 的核心:控制器(以下简称 C)负责模型(以下简称 M)和视图(以下简称 V)的交互。 |
| 7 | + |
| 8 | +这里所说的 M,通常不是一个单独的类,很多情况下它是由多个类构成的一个层。最上层的通常是以 `Model` 结尾的类,它直接被 C 持有。`Model` 类还可以持有两个对象: |
| 9 | + |
| 10 | +1. Item:它是实际存储数据的对象。它可以理解为一个字典,和 V 中的属性一一对应 |
| 11 | +2. Cache:它可以缓存自己的 Item(如果有很多) |
| 12 | + |
| 13 | +常见的误区: |
| 14 | + |
| 15 | +1. 一般情况下数据的处理会放在 M 而不是 C(C 只做不能复用的事) |
| 16 | +2. 解耦不只是把一段代码拿到外面去。而是关注是否能合并重复代码, 并且有良好的拖展性。 |
| 17 | + |
| 18 | +# 原始版 |
| 19 | + |
| 20 | +在 C 中,我们创建 `UITableView` 对象,然后将它的数据源和代理设置为自己。也就是自己管理着 UI 逻辑和数据存取的逻辑。在这种架构下,主要存在这些问题: |
| 21 | + |
| 22 | +1. 违背 MVC 模式,现在是 V 持有 C 和 M。 |
| 23 | +2. C 管理了全部逻辑,耦合太严重。 |
| 24 | +3. 其实绝大多数 UI 相关都是由 Cell 而不是 `UITableView` 自身完成的。 |
| 25 | + |
| 26 | +为了解决这些问题,我们首先弄明白,数据源和代理分别做了那些事。 |
| 27 | + |
| 28 | +### 数据源 |
| 29 | + |
| 30 | +它有两个必须实现的代理方法: |
| 31 | + |
| 32 | +```objc |
| 33 | +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section; |
| 34 | +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath; |
| 35 | +``` |
| 36 | + |
| 37 | +简单来说,只要实现了这个两个方法,一个简单的 `UITableView` 对象就算是完成了。 |
| 38 | + |
| 39 | +除此以外,它还负责管理 `section` 的数量,标题,某一个 `cell` 的编辑和移动等。 |
| 40 | + |
| 41 | +### 代理 |
| 42 | + |
| 43 | +代理主要涉及以下几个方面的内容: |
| 44 | + |
| 45 | +1. cell、headerView 等展示前、后的回调。 |
| 46 | +2. cell、headerView 等的高度,点击事件。 |
| 47 | + |
| 48 | +最常用的也是两个方法: |
| 49 | + |
| 50 | +```objc |
| 51 | +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath; |
| 52 | +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath; |
| 53 | +``` |
| 54 | + |
| 55 | +提醒:绝大多数代理方法都有一个 `indexPath` 参数 |
| 56 | + |
| 57 | +# 优化数据源 |
| 58 | + |
| 59 | +最简单的思路是单独把数据源拿出来作为一个对象。 |
| 60 | + |
| 61 | +这种写法有一定的解耦作用,同时可以有效减少 C 中的代码量。然而总代码量会上升。我们的目标是减少不必要的代码。 |
| 62 | + |
| 63 | +比如获取每一个 `section` 的行数,它的实现逻辑总是高度类似。然而由于数据源的具体实现方式不统一,所以每个数据源都要重新实现一遍。 |
| 64 | + |
| 65 | +### SectionObject |
| 66 | + |
| 67 | +首先我们来思考一个问题,数据源作为 M,它持有的 Item 长什么样?答案是一个二维数组,每个元素保存了一个 `section` 所需要的全部信息。因此除了有自己的数组(给cell用)外,还有 section 的标题等,我们把这样的元素命名为 `SectionObject`: |
| 68 | + |
| 69 | +``` |
| 70 | +@interface KtTableViewSectionObject : NSObject |
| 71 | +
|
| 72 | +@property (nonatomic, copy) NSString *headerTitle; // UITableDataSource 协议中的 titleForHeaderInSection 方法可能会用到 |
| 73 | +@property (nonatomic, copy) NSString *footerTitle; // UITableDataSource 协议中的 titleForFooterInSection 方法可能会用到 |
| 74 | +
|
| 75 | +@property (nonatomic, retain) NSMutableArray *items; |
| 76 | +
|
| 77 | +- (instancetype)initWithItemArray:(NSMutableArray *)items; |
| 78 | +
|
| 79 | +@end |
| 80 | +``` |
| 81 | + |
| 82 | +### Item |
| 83 | + |
| 84 | +其中的 `items` 数组,应该存储了每个 cell 所需要的 `Item`,考虑到 `Cell` 的特点,基类的 `BaseItem` 可以设计成这样: |
| 85 | + |
| 86 | +```objc |
| 87 | +@interface KtTableViewBaseItem : NSObject |
| 88 | + |
| 89 | +@property (nonatomic, retain) NSString *itemIdentifier; |
| 90 | +@property (nonatomic, retain) UIImage *itemImage; |
| 91 | +@property (nonatomic, retain) NSString *itemTitle; |
| 92 | +@property (nonatomic, retain) NSString *itemSubtitle; |
| 93 | +@property (nonatomic, retain) UIImage *itemAccessoryImage; |
| 94 | + |
| 95 | +- (instancetype)initWithImage:(UIImage *)image Title:(NSString *)title SubTitle:(NSString *)subTitle AccessoryImage:(UIImage *)accessoryImage; |
| 96 | + |
| 97 | +@end |
| 98 | +``` |
| 99 | +
|
| 100 | +### 父类实现代码 |
| 101 | +
|
| 102 | +规定好了统一的数据存储格式以后,我们就可以考虑在基类中完成某些方法了。以 `- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section` 方法为例,它可以这样实现: |
| 103 | +
|
| 104 | +```objc |
| 105 | +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { |
| 106 | + if (self.sections.count > section) { |
| 107 | + KtTableViewSectionObject *sectionObject = [self.sections objectAtIndex:section]; |
| 108 | + return sectionObject.items.count; |
| 109 | + } |
| 110 | + return 0; |
| 111 | +} |
| 112 | +``` |
| 113 | + |
| 114 | +比较困难的是创建 `cell`,因为我们不知道 `cell` 的类型,自然也就无法调用 `alloc` 方法。除此以外,`cell` 除了创建,还需要设置 UI,这些都是数据源不应该做的事。 |
| 115 | + |
| 116 | +这两个问题的解决方案如下: |
| 117 | + |
| 118 | +1. 定义一个协议,父类返回基类 `Cell`,子类视情况返回合适的类型。 |
| 119 | +2. 为 `Cell` 添加一个 `setObject` 方法,用于解析 Item 并更新 UI。 |
| 120 | + |
| 121 | +### 优势 |
| 122 | + |
| 123 | +经过这一番折腾,好处是相当明显的: |
| 124 | + |
| 125 | +1. 子类的数据源只需要实现 `cellClassForObject` 方法即可。原来的数据源方法已经在父类中被统一实现了。 |
| 126 | +2. 每一个 Cell 只要写好自己的 `setObject` 方法,然后坐等自己被创建,被调用这个方法即可。 |
| 127 | +3. 子类通过 `objectForRowAtIndexPath` 方法可以快速获取 item,不用重写。 |
| 128 | + |
| 129 | +对照 demo(SHA-1:6475496),感受一下效果。 |
| 130 | + |
| 131 | +# 优化代理 |
| 132 | + |
| 133 | +我们以之前所说的,代理协议中常用的两个方法为例,看看怎么进行优化与解耦。 |
| 134 | + |
| 135 | +首先是计算高度,这个逻辑并不一定在 C 完成,由于涉及到 UI,所以由 Cell 负责实现即可。而计算高度的依据就是 Object,所以我们给基类的 Cell 加上一个类方法: |
| 136 | + |
| 137 | +```objc |
| 138 | ++ (CGFloat)tableView:(UITableView*)tableView rowHeightForObject:(KtTableViewBaseItem *)object; |
| 139 | +``` |
| 140 | + |
| 141 | +另外一类问题是以处理点击事件为代表的代理方法, 它们的主要特点是都有 `indexPath` 参数用来表示位置。然而实际在处理过程中,我们并不关系位置,关心的是这个位置上的数据。 |
| 142 | + |
| 143 | +因此,我们对代理方法做一层封装,使得 C 调用的方法中都是带有数据参数的。因为这个数据对象可以从数据源拿到,所以我们需要能够在代理方法中获取到数据源对象。 |
| 144 | + |
| 145 | +为了实现这一点, 最好的办法就是继承 `UITableView`: |
| 146 | + |
| 147 | +```objc |
| 148 | +@protocol KtTableViewDelegate<UITableViewDelegate> |
| 149 | + |
| 150 | +@optional |
| 151 | + |
| 152 | +- (void)didSelectObject:(id)object atIndexPath:(NSIndexPath*)indexPath; |
| 153 | +- (UIView *)headerViewForSectionObject:(KtTableViewSectionObject *)sectionObject atSection:(NSInteger)section; |
| 154 | + |
| 155 | +// 将来可以有 cell 的编辑,交换,左滑等回调 |
| 156 | +// 这个协议继承了UITableViewDelegate ,所以自己做一层中转,VC 依然需要实现某 |
| 157 | + |
| 158 | +@end |
| 159 | + |
| 160 | +@interface KtBaseTableView : UITableView<UITableViewDelegate> |
| 161 | + |
| 162 | +@property (nonatomic, assign) id<KtTableViewDataSource> ktDataSource; |
| 163 | +@property (nonatomic, assign) id<KtTableViewDelegate> ktDelegate; |
| 164 | + |
| 165 | +@end |
| 166 | +``` |
| 167 | +
|
| 168 | +cell 高度的实现如下,调用数据源的方法获取到数据: |
| 169 | +
|
| 170 | +```ojbc |
| 171 | +- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath { |
| 172 | + id<KtTableViewDataSource> dataSource = (id<KtTableViewDataSource>)tableView.dataSource; |
| 173 | + |
| 174 | + KtTableViewBaseItem *object = [dataSource tableView:tableView objectForRowAtIndexPath:indexPath]; |
| 175 | + Class cls = [dataSource tableView:tableView cellClassForObject:object]; |
| 176 | + |
| 177 | + return [cls tableView:tableView rowHeightForObject:object]; |
| 178 | +} |
| 179 | +``` |
| 180 | + |
| 181 | +### 优势 |
| 182 | + |
| 183 | +通过对 `UITableViewDelegate` 的封装(其实主要是通过 `UITableView` 完成),我们获得了以下特性: |
| 184 | + |
| 185 | +1. C 不用关心 Cell 高度了,这个由每个 Cell 类自己负责 |
| 186 | +2. 如果数据本身存在数据源中,那么在代理协议中它可以被传给 C,免去了 C 重新访问数据源的操作。 |
| 187 | +3. 如果数据不存在于数据源,那么代理协议的方法会被正常转发(因为自定义的代理协议继承自 `UITableViewDelegate `) |
| 188 | + |
| 189 | +对照 demo(SHA-1:ca9b261),感受一下效果。 |
| 190 | + |
| 191 | +# 更加 MVC,更加简洁 |
| 192 | + |
| 193 | +在上面的两次封装中,其实我们是把 `UITableView` 持有原生的代理和数据源,改成了 `KtTableView` 持有自定义的代理和数据源。并且默认实现了很多系统的方法。 |
| 194 | + |
| 195 | +到目前为止,看上去一切都已经完成了,然而实际上还是存在一些可以改进的地方: |
| 196 | + |
| 197 | +1. 目前仍然不是 MVC 模式! |
| 198 | +2. C 的逻辑和实现依然可以进一步简化 |
| 199 | + |
| 200 | +基于以上考虑, 我们实现一个 `UIViewController` 的子类,并且把数据源和代理封装到 C 中。 |
| 201 | + |
| 202 | +```objc |
| 203 | +@interface KtTableViewController : UIViewController<KtTableViewDelegate, KtTableViewControllerDelegate> |
| 204 | + |
| 205 | +@property (nonatomic, strong) KtBaseTableView *tableView; |
| 206 | +@property (nonatomic, strong) KtTableViewDataSource *dataSource; |
| 207 | +@property (nonatomic, assign) UITableViewStyle tableViewStyle; // 用来创建 tableView |
| 208 | + |
| 209 | +- (instancetype)initWithStyle:(UITableViewStyle)style; |
| 210 | + |
| 211 | +@end |
| 212 | +``` |
| 213 | +
|
| 214 | +为了确保子类创建了数据源,我们把这个方法定义到协议里,并且定义为 `required`。 |
| 215 | +
|
| 216 | +# 成果与目标 |
| 217 | +
|
| 218 | +现在我们梳理一下经过改造的 `TableView` 该怎么用: |
| 219 | +
|
| 220 | +1. 首先你需要创建一个继承自 `KtTableViewController` 的视图控制器,并且调用它的 `initWithStyle` 方法。 |
| 221 | + |
| 222 | + ```objc |
| 223 | + KTMainViewController *mainVC = [[KTMainViewController alloc] initWithStyle:UITableViewStylePlain]; |
| 224 | + ``` |
| 225 | +2. 在子类 VC 中实现 `createDataSource` 方法,实现数据源的绑定。 |
| 226 | +
|
| 227 | + ```objc |
| 228 | + - (void)createDataSource { |
| 229 | + self.dataSource = [[KtMainTableViewDataSource alloc] init]; // 这 一步创建了数据源 |
| 230 | + } |
| 231 | + ``` |
| 232 | + |
| 233 | +3. 在数据源中,需要指定 cell 的类型。 |
| 234 | +
|
| 235 | + ```objc |
| 236 | + - (Class)tableView:(UITableView *)tableView cellClassForObject:(KtTableViewBaseItem *)object { |
| 237 | + return [KtMainTableViewCell class]; |
| 238 | + } |
| 239 | + ``` |
| 240 | + |
| 241 | +4. 在 Cell 中,需要通过解析数据,来更新 UI 并返回自己的高度。 |
| 242 | +
|
| 243 | + ```objc |
| 244 | + + (CGFloat)tableView:(UITableView *)tableView rowHeightForObject:(KtTableViewBaseItem *)object { |
| 245 | + return 60; |
| 246 | + } |
| 247 | + // Demo 中沿用了父类的 setObject 方法。 |
| 248 | + ``` |
| 249 | +
|
| 250 | +### 下一步做什么? |
| 251 | +
|
| 252 | +关于 `TableView` 的讨论远远没有结束,我列出了以下需要解决的问题 |
| 253 | +
|
| 254 | +1. 在这种设计下,数据的回传不够方便,比如 cell 的给 C 发消息。 |
| 255 | +2. 下拉刷新与上拉加载如何集成 |
| 256 | +3. 网络请求的发起,与解析数据如何集成 |
| 257 | +
|
| 258 | +关于第一个问题,其实是普通的 MVC 模式中 V 和 C 的交互问题,可以在 Cell(或者其他类) 中添加 weak 属性达到直接持有的目的,也可以定义协议。 |
| 259 | +
|
| 260 | +问题二和三是另一大块话题,网络请求大家都会实现,但如何优雅的集成进框架,保证代码的简单和可拓展,就是一个值得深入思考,研究的问题了。我会在下次有空的时候和大家分享这个问题。 |
0 commit comments