转载

如何写好一个UITableView(上)

如何写好一个UITableView(上)

本文授权转载,作者:bestswifter(简书

本文是直播分享的简单文字整理,视频地址:优酷YouTube

Demo 地址:KtTableView

MVC

讨论解耦之前,我们要弄明白 MVC 的核心:控制器(以下简称 C)负责模型(以下简称 M)和视图(以下简称 V)的交互。

这里所说的 M,通常不是一个单独的类,很多情况下它是由多个类构成的一个层。最上层的通常是以 Model 结尾的类,它直接被 C 持有。Model 类还可以持有两个对象:

  • Item:它是实际存储数据的对象。它可以理解为一个字典,和 V 中的属性一一对应

  • Cache:它可以缓存自己的 Item(如果有很多)

常见的误区:

  • 一般情况下数据的处理会放在 M 而不是 C(C 只做不能复用的事)

  • 解耦不只是把一段代码拿到外面去。而是关注是否能合并重复代码, 并且有良好的拖展性。

原始版

在 C 中,我们创建 UITableView 对象,然后将它的数据源和代理设置为自己。也就是自己管理着 UI 逻辑和数据存取的逻辑。在这种架构下,主要存在这些问题:

  • 违背 MVC 模式,现在是 V 持有 C 和 M。

  • C 管理了全部逻辑,耦合太严重。

  • 其实绝大多数 UI 相关都是由 Cell 而不是 UITableView 自身完成的。

为了解决这些问题,我们首先弄明白,数据源和代理分别做了那些事。

数据源

它有两个必须实现的代理方法:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section; - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;

简单来说,只要实现了这个两个方法,一个简单的 UITableView 对象就算是完成了。

除此以外,它还负责管理 section 的数量,标题,某一个 cell 的编辑和移动等。

代理

代理主要涉及以下几个方面的内容:

  • cell、headerView 等展示前、后的回调。

  • cell、headerView 等的高度,点击事件。

最常用的也是两个方法:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath; - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;

提醒:绝大多数代理方法都有一个 indexPath 参数

优化数据源

最简单的思路是单独把数据源拿出来作为一个对象。

这种写法有一定的解耦作用,同时可以有效减少 C 中的代码量。然而总代码量会上升。我们的目标是减少不必要的代码。

比如获取每一个 section 的行数,它的实现逻辑总是高度类似。然而由于数据源的具体实现方式不统一,所以每个数据源都要重新实现一遍。

SectionObject

首先我们来思考一个问题,数据源作为 M,它持有的 Item 长什么样?答案是一个二维数组,每个元素保存了一个 section 所需要的全部信息。因此除了有自己的数组(给cell用)外,还有 section 的标题等,我们把这样的元素命名为 SectionObject:

@interface KtTableViewSectionObject : NSObject @property (nonatomic, copy) NSString *headerTitle; // UITableDataSource 协议中的 titleForHeaderInSection 方法可能会用到 @property (nonatomic, copy) NSString *footerTitle; // UITableDataSource 协议中的 titleForFooterInSection 方法可能会用到 @property (nonatomic, retain) NSMutableArray *items; - (instancetype)initWithItemArray:(NSMutableArray *)items; @end

Item

其中的 items 数组,应该存储了每个 cell 所需要的 Item,考虑到 Cell 的特点,基类的 BaseItem 可以设计成这样:

@interface KtTableViewBaseItem : NSObject @property (nonatomic, retain) NSString *itemIdentifier; @property (nonatomic, retain) UIImage *itemImage; @property (nonatomic, retain) NSString *itemTitle; @property (nonatomic, retain) NSString *itemSubtitle; @property (nonatomic, retain) UIImage *itemAccessoryImage; - (instancetype)initWithImage:(UIImage *)image Title:(NSString *)title SubTitle:(NSString *)subTitle AccessoryImage:(UIImage *)accessoryImage; @end

父类实现代码

规定好了统一的数据存储格式以后,我们就可以考虑在基类中完成某些方法了。以 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 方法为例,它可以这样实现:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {     if (self.sections.count > section) {         KtTableViewSectionObject *sectionObject = [self.sections objectAtIndex:section];         return sectionObject.items.count;     }     return 0; }

比较困难的是创建 cell,因为我们不知道 cell 的类型,自然也就无法调用 alloc 方法。除此以外,cell 除了创建,还需要设置 UI,这些都是数据源不应该做的事。

这两个问题的解决方案如下:

  • 定义一个协议,父类返回基类 Cell,子类视情况返回合适的类型。

  • 为 Cell 添加一个 setObject 方法,用于解析 Item 并更新 UI。

优势

经过这一番折腾,好处是相当明显的:

  • 子类的数据源只需要实现 cellClassForObject 方法即可。原来的数据源方法已经在父类中被统一实现了。

  • 每一个 Cell 只要写好自己的 setObject 方法,然后坐等自己被创建,被调用这个方法即可。

  • 子类通过 objectForRowAtIndexPath 方法可以快速获取 item,不用重写。

对照 demo(SHA-1:6475496),感受一下效果。

优化代理

我们以之前所说的,代理协议中常用的两个方法为例,看看怎么进行优化与解耦。

首先是计算高度,这个逻辑并不一定在 C 完成,由于涉及到 UI,所以由 Cell 负责实现即可。而计算高度的依据就是 Object,所以我们给基类的 Cell 加上一个类方法:

+ (CGFloat)tableView:(UITableView*)tableView rowHeightForObject:(KtTableViewBaseItem *)object;

另外一类问题是以处理点击事件为代表的代理方法, 它们的主要特点是都有 indexPath 参数用来表示位置。然而实际在处理过程中,我们并不关系位置,关心的是这个位置上的数据。

因此,我们对代理方法做一层封装,使得 C 调用的方法中都是带有数据参数的。因为这个数据对象可以从数据源拿到,所以我们需要能够在代理方法中获取到数据源对象。

为了实现这一点, 最好的办法就是继承 UITableView:

@protocol KtTableViewDelegate@optional - (void)didSelectObject:(id)object atIndexPath:(NSIndexPath*)indexPath; - (UIView *)headerViewForSectionObject:(KtTableViewSectionObject *)sectionObject atSection:(NSInteger)section; // 将来可以有 cell 的编辑,交换,左滑等回调 // 这个协议继承了UITableViewDelegate ,所以自己做一层中转,VC 依然需要实现某 @end @interface KtBaseTableView : UITableView@property (nonatomic, assign) id ktDataSource; @property (nonatomic, assign) id ktDelegate; @end

cell 高度的实现如下,调用数据源的方法获取到数据:

- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath {     id dataSource = (id)tableView.dataSource;     KtTableViewBaseItem *object = [dataSource tableView:tableView objectForRowAtIndexPath:indexPath];     Class cls = [dataSource tableView:tableView cellClassForObject:object];     return [cls tableView:tableView rowHeightForObject:object]; }

优势

通过对 UITableViewDelegate 的封装(其实主要是通过 UITableView 完成),我们获得了以下特性:

  • C 不用关心 Cell 高度了,这个由每个 Cell 类自己负责

  • 如果数据本身存在数据源中,那么在代理协议中它可以被传给 C,免去了 C 重新访问数据源的操作。

  • 如果数据不存在于数据源,那么代理协议的方法会被正常转发(因为自定义的代理协议继承自 UITableViewDelegate)

对照 demo(SHA-1:ca9b261),感受一下效果。

更加 MVC,更加简洁

在上面的两次封装中,其实我们是把 UITableView 持有原生的代理和数据源,改成了 KtTableView 持有自定义的代理和数据源。并且默认实现了很多系统的方法。

到目前为止,看上去一切都已经完成了,然而实际上还是存在一些可以改进的地方:

  • 目前仍然不是 MVC 模式!

  • C 的逻辑和实现依然可以进一步简化

基于以上考虑, 我们实现一个 UIViewController 的子类,并且把数据源和代理封装到 C 中。

@interface KtTableViewController : UIViewController@property (nonatomic, strong) KtBaseTableView *tableView; @property (nonatomic, strong) KtTableViewDataSource *dataSource; @property (nonatomic, assign) UITableViewStyle tableViewStyle; // 用来创建 tableView - (instancetype)initWithStyle:(UITableViewStyle)style; @end

为了确保子类创建了数据源,我们把这个方法定义到协议里,并且定义为 required。

成果与目标

现在我们梳理一下经过改造的 TableView 该怎么用:

  • 首先你需要创建一个继承自 KtTableViewController 的视图控制器,并且调用它的 initWithStyle 方法。

KTMainViewController *mainVC = [[KTMainViewController alloc] initWithStyle:UITableViewStylePlain];
  • 在子类 VC 中实现 createDataSource 方法,实现数据源的绑定。

- (void)createDataSource {     self.dataSource = [[KtMainTableViewDataSource alloc] init]; // 这    一步创建了数据源 }
  • 在数据源中,需要指定 cell 的类型。

- (Class)tableView:(UITableView *)tableView cellClassForObject:(KtTableViewBaseItem *)object {     return [KtMainTableViewCell class]; }
  • 在 Cell 中,需要通过解析数据,来更新 UI 并返回自己的高度。

+ (CGFloat)tableView:(UITableView *)tableView rowHeightForObject:(KtTableViewBaseItem *)object {     return 60; } // Demo 中沿用了父类的 setObject 方法。

下一步做什么?

关于TableView的讨论远远没有结束,我列出了以下需要解决的问题

  • 在这种设计下,数据的回传不够方便,比如 cell 的给 C 发消息。

  • 下拉刷新与上拉加载如何集成

  • 网络请求的发起,与解析数据如何集成

关于第一个问题,其实是普通的 MVC 模式中 V 和 C 的交互问题,可以在 Cell(或者其他类) 中添加 weak 属性达到直接持有的目的,也可以定义协议。

问题二和三是另一大块话题,网络请求大家都会实现,但如何优雅的集成进框架,保证代码的简单和可拓展,就是一个值得深入思考,研究的问题了。我会在下次有空的时候和大家分享这个问题。

正文到此结束
Loading...