Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

读 『如何写好一个UITableView』 #2

Open
ShannonChenCHN opened this issue Oct 5, 2017 · 1 comment
Open

读 『如何写好一个UITableView』 #2

ShannonChenCHN opened this issue Oct 5, 2017 · 1 comment
Labels

Comments

@ShannonChenCHN
Copy link
Owner

ShannonChenCHN commented Oct 5, 2017

这篇文章主要讲解了如何写好一个列表这个最熟悉的主题,分别讨论了以下几个问题:

  • 如何优雅地处理 UITableView 的代理方法和数据源方法,以提高代码的复用性和可维护性
  • 网络请求和解析数据的逻辑该怎么处理
  • 下拉刷新和上拉加载更多的逻辑怎么处理

核心问题是如何减轻业务方的负责。

@ShannonChenCHN
Copy link
Owner Author

『如何写好一个UITableView』

一、痛点

  • UITableView 的代理方法和数据源方法
  • 网络请求、解析数据
  • 下拉、上拉

二、MVC

  • 控制器 controller 负责 model 和 view 的交互
  • Model 不仅仅是一个 model 类,而是一个 model 层
  • Controller 只做不能复用的事情?
  • 解耦不仅仅是把代码拆开,更应该是关注代码结构的可重用性、可维护性、可扩展性。

三、传统的做法

现状

Controller 中实现 delegate 和 dataSource 方法,管理 UI 和数据、交互。

  • 违背 MVC 模式,现在是 V 持有 C 和 M(?)
  • C 管理了全部逻辑,耦合太严重
  • 其实绝大多数 UI 相关都是由 Cell 而不是 UITableView 自身完成的(?)

数据源

总共有以下几类方法:

  • Row count
  • Row display
  • Editing
  • Moving/reordering
  • Index
  • Data manipulation

其中两个最常用的方法:

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

代理

总共有以下几类方法:

  • Display customization
  • Variable height support
  • Section header & footer information
  • Accessories (disclosures)
  • Selection
  • Editing
  • Moving/reordering
  • Indentation
  • Copy/Paste
  • Focus

其中以下几个方法最常用:

- (nullable UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section;   
- (nullable UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section;

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section;
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section;

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

四、优化

1. 数据源

  • 基本思想:将数据源方法抽取出来到一个单独的类中

  • 包含哪些内容:

    • 每个 DataSource 都应该有一个 SectionModels 数组,一个 Section 对应一个 SectionModel
    • 每个 SectionModel 中又包含 Header、Footer、Cell 相关的数据,其中这个 section 中所有的 cell 数据都放到 CellModels 数组中
    • CellModel 中可以保存 cell reuse identifier 和 cell height 缓存、以及 cell 展示信息等数据
  • 实现哪些方法:下面三个方法都要在这个数据源类中实现

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView;
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
  • 自定义一个继承于 UITableViewDataSource 的 dataSource 协议,包含两个方法
// 每个 indexPath 对应的 cell 的 class,主要是用来给 `cellForRow` 方法创建 cell 用的,子类需要重写
- (Class)tableView:(UITableView*)tableView cellClassForModel:(SCTableViewCellModel *)model;

// 提供一个接口用于获取指定 indexPath 对应的 model
- (SCTableViewCellModel *)tableView:(UITableView *)tableView modelForRowAtIndexPath:(NSIndexPath *)indexPath;
  • 自定义 Cell 要做的事情:实现 -setModel 方法,或者提供 model 属性,用来接收 model 数据

2. 代理

  • 两个问题
    • cell 高度:跟 UI 和数据相关,应该交给 cell 自己去计算,提供 model 就行了----> cell 中提供一个计算高度的静态方法
    • 点击事件:主要是根据 model 来处理,位置并不太需要关心---->定义一个继承于 UITableViewDelegate 的协议,提供一个用于处理点击事件的方法,作为系统 cell 点击方法的中转,顺便将 model 传递出来

3.如何将上面针对数据源和代理的优化串联起来

  • 定义一个继承于 UITableView 的自定义 table view
  • 定义两个属性 customDelegatecustomDataSource,分别用来接收自定义数据源和自定义代理,如果数据源中有数据,自定义 table view 可以直接在内部处理一些系统的代理方法,如果没有,也可以交给外面的 controller 自己去实现
  • 设置 tableView 的 delegate 为自定义 table view 自身
  • 重写 customDataSource 的 setter 方法,将 dataSource 中转给 UITableViewdataSource 属性
  • 在自定义 table view 中实现 UITableViewDelegate 的方法,用来计算 cell 高度,和分发点击事件等

4. 成果

  • 此时的 MVC
    • M 就是数据源及相关的 Model 类
    • V 就是自定义的 table view
    • C 就是一个更清爽的 controller,只负责创建数据源、table view,以及处理一些不能复用的逻辑
  • 解决了什么痛点
    • 不再需要每次写一大堆重复的 UITableView 代理方法和数据源方法
    • 实现简单的数据绑定,不再需要关注代理方法和数据源方法,只需要关注数据源(Model)和 cell 自身(View)
    • 更清晰的 MVC
  • 如何使用
    • 创建你的 View Controller,创建数据源对象,实现“数据绑定”
    • 创建一个数据源子类,在数据源中重写一些基类的方法,比如 cell 的 class
    • 创建自定义 cell 的子类,实现 setModel 方法、cell 高度计算的方法

5. 下一步

  • 通信:cell 如何和 controller 通信 ——> 代理回调或者弱持有 controller(是不是可以搞个类似于 router 那种中间层呢)
  • 下拉刷新和上拉加载的处理
  • 网络请求、数据解析的处理
  • (网络错误或者数据为空时的占位图)

五、网络层

1. 为什么需要网络层

  • 考虑到后期更新——第三方网络库的迁移,比如由 ASI 迁移到 AFNetworking
  • 需要第三方网络库基础上添加一些扩展功能,比如请求的取消,翻页,计算请求耗时,对 header 的处理等等
  • 其他跟网络请求有关的自定义扩展,请求失败的 toast,空态占位图,log 等等

2. 三个环节

  • 发起请求
    • 集约式(命令式):只定义一个类,提供接口接受参数来发请求
    • 分布式(声明式):定义一个基类,然后针对各个 API 再分别创建对应的 API Manager 子类
  • 回调处理
    • block
    • delegate
  • 数据解析

3. 实现

  • 定义一个 API Manager 类
    • 封装网络请求的具体实现,让调用更简单
    • 扩展一些自定义功能:网络请求的状态、返回时的数据处理
    • 处理一些公共逻辑:耗时统计等
  • 定义 BaseItem:主要用来 JSON 解析,保存解析后的数据
  • 定义 BaseModel
    • 管理对应 API 的网络请求
    • 管理数据模型 BaseItem

4. 实践

  • 定义 BaseItem 的子类
  • 定义 BaseModel 的子类,并持有 Item 和 API Manager
  • 在 controller 中创建所需的 Model 对象,发起网络请求,并添加处理回调的逻辑,回调时可以从 Model 中获取到解析后 Item 数据

六、下拉刷新和上拉加载更多

1. 两个问题

  • 封装 UI 细节
  • 翻页

2.实现

  • 继承 baseTableController,提供设置上拉下拉控件的接口和处理一些基础逻辑(比如 endRefresh 操作), 并在自定义 tableView 中处理上下拉控件的添加细节
  • 继承 BaseModel,实现翻页的逻辑

七、总结

  • 没有最通用的架构,只有最合适的架构
  • 实际情况下,架构的设计应该是自顶向下的,先有了问题和需求,再一层一层抽象

一些思考:
核心思想:复用+解耦+易于理解、逻辑清晰

  1. 值得借鉴之处:
  2. 不同看法:
  3. 补充:
  • 空态页
  • 网络错误提示 toast
  • 页面缓存

@ShannonChenCHN ShannonChenCHN changed the title 读 『如何写好一个UITableView』 Oct 5, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant