综述
讲到 UITableView
,大家一定都不陌生。有一个相对夸张的说法,叫做学好 UITableView
,你就是一名合格的iOS
工程师
闲话少说,最近在写 Swift
的过程中碰到了以下几个问题,特别在此记录。
遇到的问题
-
cellForRowAtIndexPath
代理中,对cell
(尤其是自定义cell
) 的初始化异同- 和
OC
的区别 —— 不能使用OC
的那种判空方式来初始化 - 初始化不能使用自定义的方法 —— 通过
dequeue
方法得到的cell
永远都是非空的,换言之,即便你自定义了一个初始化方法,它也不会被执行到。 - 通过渲染方式(render)来绘制图像,赋值
- 理解
cell
的复用机制
- 和
- 刷新的问题
- 使用
reloadData
时候,在iOS 11
上会产生抖动 -
insertRow
和deleteRow
和reloadRows
一样都属于局部刷新的范畴,局部刷新时,系统会创建一个新的cell
来,并和旧的cell
在刷新时来回切换。
- 使用
先明确几个概念
- 代码中的
setup
表示只会执行一次,而且在 cell 的初始化中表示他的绘图(不带数据)也只会执行一次 - 代码中的
render
表示渲染,实际上是意味着setup
已经完成了绘图,我要在每次重用时把数据传进去渲染
重申 Cell 的复用机制和使用
简单的来说,tableview 的复用机制是我们在 cellForRowAtIndexPath
的一系列操作。
-
Cell
的UI
一旦被创建,系统就会存放在复用池中等待复用。 -
Cell
的可变内容(通常是label
的text
,image
的内容,选中的背景色等),是不会记录的。 - 删除某个
Cell
后再创建一个新的Cell
, 实际上你会发现新的Cell
中有部分UI
时旧Cell
中的 -
reloadRows
局部刷新时会创建新的Cell
,再刷新时会和旧的Cell
来回切换
很简单的情况是,如果我们不每次滚动的时候去dataSource
数组中把对应index
的数值取出来,只管的感受就是UI
虽然固定,但是数据和图片一直在乱跑
鉴于Swift
无法自定义cell
的初始化,那么上下滚动时,怎么重新赋值而不重复绘制就显得格外重要。
关于 cellForRowAtIndexPath
的初始化问题其实在这篇文章中已经讨论过,这里不作赘述
Swift 踩坑笔记(二)—— 初始化Tableview 及自定义 TableviewCell
我们要讨论的是在Cell
复用过程中的赋值和 UI 重叠的问题。
典型案例 —— Cell 的 UI 内容根据数据而定
描述
根据上面所说的,Cell
的UI
在被创建后,就会被放进复用池中,等待被重用。但是如果像下面这种情况:
一个TableView
中每个Cell
的内容是根据数据中数组的个数来渲染的,就会出问题:
我们这里的
Cell
分了很多层级,
除了顶部的 Header
区域是固定知道的高度外,下面的 区域 InfoA, InfoB, InfoC ...
等等,都是根据具体的信息去绘制的。
换言之,我不知道每个 Cell 具体要画几个 InfoX
这样会造成一个很大的问题:
- 因为根据复用机制,数据是每次都有可能不同的,而根据数据创建的 UI 一旦被创建,就会一直存在于复用池中。
- 如果
Cell
发生了删除,再添加,就有可能将那些不用的Cell UI
复用进来。 - 局部刷新时会创建新的
Cell
,这时候叠加在旧的UI
上切换时,就会造成视图的重叠
来看下错误的现象图
局部刷新的效果
使用 reveal 查看,发现多了一个层级UI,盖在应该有的位置()
正确的代码
为了避免混淆,我这里就不贴原来错误的代码了。
来看下面正确的代码
// 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
}
思路:
- 上面的图中,
Header
的部分是固定的,也就是不是动态变化的 UI,因此每次render
的时候只要重新赋值即可- 而下面的
infoA, infoB, infoC...
是根据数值来变化的。我们现在能做的就是对于动态的Cell UI
,先把这几个subView
都removeFromSuperView
避免干扰,然后setUp
重绘一次,再render
进赋值。
再来看下面的这段 自定义 Cell
的代码
// 略去类的初始化,这里为了 render ,去持有静态的 UI
private var headerBaseInfoView: BaseInfoView = BaseInfoView()
public func renderCell(info: accountModel) {
// 除了静态的 UI,剩下的都remove 掉,避免重用时的干扰
for view in contentView.subviews {
guard view != headerBaseInfoView else {
continue
}
view.removeFromSuperview()
}
headerBaseInfoView.render(renderInfo: info.baseInfo!)
setupAndRenderInfoViews(bindInfos)
}
private func setupAndRenderInfoViews(_ bindInfos: [infoModel]) {
var infoViews: [infoView] = []
for (index, bindInfo) in bindInfos.enumerated() {
// 创建后渲染数据
let bindInfoView = InfoView()
bindInfoView.render(bindInfo: bindInfo)
// 布局 (也可以先布局再渲染数据,这无所谓)
contentView.addSubview(bindInfoView)
bindInfoView.snp.makeConstraints { (make) in
//这里略去约束的部分
}
infoViews.append(bindInfoView)
}
}
下面是讲解:
- 类中要去持有静态的视图,作为属性内容。
- headerBaseInfoView 是固定的内容,所以实际上我们在重写他的初始化方法的时候,直接就把
setupUI()
(只会执行一次)这个绘图的工作做掉了 - infoViews 属于我一开始没办法知道你有几个,所以我无法初始化。只在每次渲染数据的时候:
- 先将所有动态视图
remove
掉 - 根据数据内容重新渲染视图并赋值(也可以先赋值再渲染数据,不影响)
- 先将所有动态视图
刷新的问题
先来说说 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()
实际上和 reload 没有太多的差异,只是注意局部刷新,会创建新的Cell
。
下面两篇文章也提到了类似的问题。
参考文章一
慎用局部刷新
因为之前对重用机制的理解存在误区,所以文章内容更新了。