综述
讲到 UITableView
,大家一定都不陌生。有一个相对夸张的说法,叫做学好 UITableView
,你就是一名合格的iOS
工程师
闲话少说,最近在写 Swift
的过程中碰到了以下几个问题,特别在此记录。
遇到的问题
cellForRowAtIndexPath
代理中,对cell
(尤其是自定义cell
) 的初始化异同- 和
OC
的区别 —— 不能使用OC
的那种判空方式来初始化 - 初始化不能使用自定义的方法 —— 通过
dequeue
方法得到的cell
永远都是非空的,换言之,即便你自定义了一个初始化方法,它也不会被执行到。 - 通过渲染方式(render)来绘制图像,赋值
- 和
- 刷新的问题
- 使用
reloadData
时候,在iOS 11
上会产生抖动 - 慎用局部刷新
reloadRows
的相关方法,会造成cell
复用混乱 insertRow
和deleteRow
和reloadRows
一样都属于局部刷新的范畴,需要用beginUpdate
和endUpdate
包起来。这上上一点类似
- 使用
先明确两个概念
- 代码中的
setup
表示只会执行一次,而且在 cell 的初始化中表示他的绘图(不带数据)也只会执行一次 - 代码中的
render
表示渲染,实际上是意味着setup
已经完成了绘图,我要在每次重用时把数据传进去渲染
初始化问题 cellForRowAtIndexPath
关于 cellForRowAtIndexPath
的初始化问题其实在这篇文章中已经讨论过,这里不作赘述 Swift 踩坑笔记(二)—— 初始化Tableview 及自定义 TableviewCell
我们要讨论的是在cell
复用过程中的赋值问题。
简单的来说,tableview 的复用机制是我们在 cellForRowAtIndexPath
初始化时绘制好必要的控件及相关约束,但是并不会去赋值。因为每次上下滚动都会重新从复用池中取出 cell,将 DataSource 对应的数据赋值一次. 鉴于Swift
无法自定义cell
的初始化,那么上下滚动时,怎么重新赋值而不重复绘制就显得格外重要。
来看下面的代码
// tableview 代理
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: someCellID, for: indexPath) as! MyCell
cell.renderCell(info: dataSource[indexPath.row])
return cell
}
复制代码
再来看下面的Cell
示意图
我们这里的 Cell
分了很多层级,
除了顶部的 Header
区域是固定知道的高度外,下面的 区域 InfoA, InfoB, InfoC ...
等等,都是根据具体的信息去绘制的。 换言之,我不知道每个 Cell 具体要画几个 InfoX
普通的cell
初始化,只能满足固定的视图,这种动态的,只能用cell
自定义一个render
方法来做了。
再来看下面的这段 自定义 Cell
的代码
// 略去类的初始化,这里为了 render 的目的,必须去持有这两块视图
private var headerBaseInfoView: IGGIDBaseInfoView
private var infoViews: [infoView] = []
public func renderCell(info: IGGIDAccountModel) {
headerBaseInfoView.render(renderInfo: info.baseInfo)
renderInfoViews(info.infos)
}
private func renderInfoViews(_ infos: [someInfoModel]?) {
guard let infos = infos, infos.count > 0 else { return }
// 如果已经创建过了,那么只要赋值就可以了
if (infoViews.count > 0) {
for (index, bindInfo) in bindInfos.enumerated() {
let infoView = infoViews[index]
infoView.render(info)
}
} else {
setupInfoViews(infos) // 如果没有持有,表示初次渲染,这时候要赋值
}
}
private func setupPlatformBindInfoViews(_ infos: Array<Info>) {
for (index, bindInfo) in infos.enumerated() {
let infoView = InfoView()
containerView.addSubview(infoView)
infoView.snp.makeConstraints { (make) in
//... 写约束
}
infoViews.append(infoView)
infoView.render(info) // 记得再去调用一次
}
}
复制代码
下面是讲解:
- 类中要去持有那些会被渲染的视图,作为属性内容。
- headerBaseInfoView 是固定的内容,所以实际上我们在重写他的初始化方法的时候,直接就把
setupUI()
(只会执行一次)这个绘图的工作做掉了 - infoViews 属于我一开始没办法知道你有几个,所以我无法初始化。因此我只能在
render
的时候做下面两个操作:- 先对
infoViews
判空,不存在,那么就做第一次的setupUI
操作,这时候根据model
也已经知道数量了,先把绘图工作做了。- 绘图完毕之后,也要做一次正规的
render
操作,不然会没数据,变成空白
- 绘图完毕之后,也要做一次正规的
- 完成了
setupUI
之后,刷新数据
- 先对
综上所述,就是和概念中说的一样:
cell
的操作都是先通过setupUI
绘制视图,然后去render
把数值赋过去。只是一些条件下,我们不知道要画几个,那么只能在render
的时候根据当前数据,补上setupUI
之后,再去做真正的render
刷新的问题
先来说说 reloadData
的缺点
-
性能问题 我们都知道,
UITableview
中reloadData
是需要慎用的。因为他会将整个tableview
都刷新一遍。这意味着也许我只需要刷新2个cell
,你却让所有的cell
都重渲染了一遍。从性能而言这显然是不可取的。 所以我们才会想到去用局部刷新。 -
reloadData
无法像系统提供的其他刷新方法一样,带有animate
参数,这让刷新时,整个页面看起来非常突兀。如果你不自己加动画,那么体验真的不太好 -
在
iOS 11
上会有一个问题,就是重载之后页面会乱跑:-
解决办法:
google
后,得到的内容是说 Self-Sizing在iOS11下是默认开启的,Headers, footers, and cells都默认开启Self-Sizing,所有estimated 高度默认值从iOS11之前的 0 改变为UITableViewAutomaticDimension
if #available(iOS 11.0, *) { taleview.estimatedRowHeight = 0 taleview.estimatedSectionHeaderHeight = 0 taleview.estimatedSectionFooterHeight = 0 } 复制代码
-
局部刷新的问题
鉴于上面讲的reloadData
,我们很自然的就会想到使用局部刷新来做。
tableview.beginUpdates()
tableview.reloadRows(at: tableview.indexPathsForVisibleRows!, with: .none)
tableview.endUpdates()
复制代码
然而,事实比我们想象的要残酷:
局部刷新的效果
使用 reveal 查看,发现多了一个层级,盖在应该有的位置
我查看了官方的文档,也自己做了断点调试,发现:
- reloadData 的重载,会把复用池中的 cell 按需要重新拿出来用一次。
不会创建新的cell
- reloadRows 的重载,会重新创建两个新的
cell
. 创建就创建了吧,可是你为啥在复用的时候,视图的渲染出了问题?
目前来看,似乎是OC
下不会出问题,swift 3
(我用的是4.0的版本)下会有这个 bug。 目前还是先使用reloadData
的全局刷新替换局部刷新,希望后续会有更好的办法
如果有更好的办法解决,欢迎告知。