Skip to content

Commit d4a61eb

Browse files
committed
添加 README,其他小优化
1 parent fa67436 commit d4a61eb

3 files changed

Lines changed: 267 additions & 1 deletion

File tree

KtTableView/KtTableView/KtMainTableViewCell.m

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,8 @@ - (void)setSelected:(BOOL)selected animated:(BOOL)animated {
2121
// Configure the view for the selected state
2222
}
2323

24+
+ (CGFloat)tableView:(UITableView *)tableView rowHeightForObject:(KtTableViewBaseItem *)object {
25+
return 60;
26+
}
27+
2428
@end

KtTableView/KtTableView/KtTableViewController.m

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ - (instancetype)initWithStyle:(UITableViewStyle)style {
2121

2222
// 这个方法实际上要被子类重写,生成对应类型的 data source
2323
- (void)createDataSource {
24-
self.dataSource = [[KtTableViewDataSource alloc] init];
24+
@throw [NSException exceptionWithName:@"Cann't use this method"
25+
reason:@"You can only call this method in subclass"
26+
userInfo:nil];
2527
}
2628

2729
- (void)viewDidLoad {

KtTableView/README.md

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
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

Comments
 (0)