綜述
講到 UITableView
,大家一定都不陌生算墨。有一個相對夸張的說法净嘀,叫做學(xué)好 UITableView
侠讯,你就是一名合格的iOS
工程師
閑話少說厢漩,最近在寫 Swift
的過程中碰到了以下幾個問題,特別在此記錄柴底。
遇到的問題
-
cellForRowAtIndexPath
代理中柄驻,對cell
(尤其是自定義cell
) 的初始化異同- 和
OC
的區(qū)別 —— 不能使用OC
的那種判空方式來初始化 - 初始化不能使用自定義的方法 —— 通過
dequeue
方法得到的cell
永遠都是非空的焙压,換言之,即便你自定義了一個初始化方法野哭,它也不會被執(zhí)行到拨黔。 - 通過渲染方式(render)來繪制圖像绰沥,賦值
- 理解
cell
的復(fù)用機制
- 和
- 刷新的問題
- 使用
reloadData
時候徽曲,在iOS 11
上會產(chǎn)生抖動 -
insertRow
和deleteRow
和reloadRows
一樣都屬于局部刷新的范疇,局部刷新時涧衙,系統(tǒng)會創(chuàng)建一個新的cell
來,并和舊的cell
在刷新時來回切換雁比。
- 使用
先明確幾個概念
- 代碼中的
setup
表示只會執(zhí)行一次章贞,而且在 cell 的初始化中表示他的繪圖(不帶數(shù)據(jù))也只會執(zhí)行一次 - 代碼中的
render
表示渲染非洲,實際上是意味著setup
已經(jīng)完成了繪圖两踏,我要在每次重用時把數(shù)據(jù)傳進去渲染
重申 Cell 的復(fù)用機制和使用
簡單的來說,tableview 的復(fù)用機制是我們在 cellForRowAtIndexPath
的一系列操作赡麦。
-
Cell
的UI
一旦被創(chuàng)建泛粹,系統(tǒng)就會存放在復(fù)用池中等待復(fù)用肮疗。 -
Cell
的可變內(nèi)容(通常是label
的text
,image
的內(nèi)容们衙,選中的背景色等)蒙挑,是不會記錄的愚臀。 - 刪除某個
Cell
后再創(chuàng)建一個新的Cell
, 實際上你會發(fā)現(xiàn)新的Cell
中有部分UI
時舊Cell
中的 -
reloadRows
局部刷新時會創(chuàng)建新的Cell
,再刷新時會和舊的Cell
來回切換
很簡單的情況是馋袜,如果我們不每次滾動的時候去dataSource
數(shù)組中把對應(yīng)index
的數(shù)值取出來桃焕,只管的感受就是UI
雖然固定捧毛,但是數(shù)據(jù)和圖片一直在亂跑
鑒于Swift
無法自定義cell
的初始化呀忧,那么上下滾動時,怎么重新賦值而不重復(fù)繪制就顯得格外重要胰坟。
關(guān)于 cellForRowAtIndexPath
的初始化問題其實在這篇文章中已經(jīng)討論過泞辐,這里不作贅述
Swift 踩坑筆記(二)—— 初始化Tableview 及自定義 TableviewCell
我們要討論的是在Cell
復(fù)用過程中的賦值和 UI 重疊的問題。
典型案例 —— Cell 的 UI 內(nèi)容根據(jù)數(shù)據(jù)而定
描述
根據(jù)上面所說的吹缔,Cell
的UI
在被創(chuàng)建后厢塘,就會被放進復(fù)用池中肌幽,等待被重用。但是如果像下面這種情況:
一個TableView
中每個Cell
的內(nèi)容是根據(jù)數(shù)據(jù)中數(shù)組的個數(shù)來渲染的格嘁,就會出問題:
我們這里的
Cell
分了很多層級讥蔽,
除了頂部的 Header
區(qū)域是固定知道的高度外冶伞,下面的 區(qū)域 InfoA, InfoB, InfoC ...
等等步氏,都是根據(jù)具體的信息去繪制的。
換言之芋类,我不知道每個 Cell 具體要畫幾個 InfoX
這樣會造成一個很大的問題:
- 因為根據(jù)復(fù)用機制侯繁,數(shù)據(jù)是每次都有可能不同的泡躯,而根據(jù)數(shù)據(jù)創(chuàng)建的 UI 一旦被創(chuàng)建丽焊,就會一直存在于復(fù)用池中技健。
- 如果
Cell
發(fā)生了刪除雌贱,再添加偿短,就有可能將那些不用的Cell UI
復(fù)用進來。 - 局部刷新時會創(chuàng)建新的
Cell
导街,這時候疊加在舊的UI
上切換時纤子,就會造成視圖的重疊
來看下錯誤的現(xiàn)象圖
局部刷新的效果
使用 reveal 查看控硼,發(fā)現(xiàn)多了一個層級UI,蓋在應(yīng)該有的位置()
正確的代碼
為了避免混淆翼悴,我這里就不貼原來錯誤的代碼了鹦赎。
來看下面正確的代碼
// 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
的部分是固定的锁施,也就是不是動態(tài)變化的 UI悉抵,因此每次render
的時候只要重新賦值即可- 而下面的
infoA, infoB, infoC...
是根據(jù)數(shù)值來變化的。我們現(xiàn)在能做的就是對于動態(tài)的Cell UI
傻谁,先把這幾個subView
都removeFromSuperView
避免干擾列粪,然后setUp
重繪一次,再render
進賦值力图。
再來看下面的這段 自定義 Cell
的代碼
// 略去類的初始化吃媒,這里為了 render ,去持有靜態(tài)的 UI
private var headerBaseInfoView: BaseInfoView = BaseInfoView()
public func renderCell(info: accountModel) {
// 除了靜態(tài)的 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() {
// 創(chuàng)建后渲染數(shù)據(jù)
let bindInfoView = InfoView()
bindInfoView.render(bindInfo: bindInfo)
// 布局 (也可以先布局再渲染數(shù)據(jù)氯质,這無所謂)
contentView.addSubview(bindInfoView)
bindInfoView.snp.makeConstraints { (make) in
//這里略去約束的部分
}
infoViews.append(bindInfoView)
}
}
下面是講解:
- 類中要去持有靜態(tài)的視圖闻察,作為屬性內(nèi)容。
- headerBaseInfoView 是固定的內(nèi)容呢灶,所以實際上我們在重寫他的初始化方法的時候钉嘹,直接就把
setupUI()
(只會執(zhí)行一次)這個繪圖的工作做掉了 - infoViews 屬于我一開始沒辦法知道你有幾個,所以我無法初始化缨睡。只在每次渲染數(shù)據(jù)的時候:
- 先將所有動態(tài)視圖
remove
掉 - 根據(jù)數(shù)據(jù)內(nèi)容重新渲染視圖并賦值(也可以先賦值再渲染數(shù)據(jù)奖年,不影響)
- 先將所有動態(tài)視圖
刷新的問題
先來說說 reloadData
的缺點
性能問題
我們都知道沛贪,UITableview
中reloadData
是需要慎用的。因為他會將整個tableview
都刷新一遍嗅义。這意味著也許我只需要刷新2個cell
隐砸,你卻讓所有的cell
都重渲染了一遍季希。從性能而言這顯然是不可取的幽纷。
所以我們才會想到去用局部刷新友浸。reloadData
無法像系統(tǒng)提供的其他刷新方法一樣偏窝,帶有animate
參數(shù),這讓刷新時伦意,整個頁面看起來非常突兀驮肉。如果你不自己加動畫已骇,那么體驗真的不太好-
在
iOS 11
上會有一個問題,就是重載之后頁面會亂跑:
頁面亂跑.gif-
解決辦法:
google
后奈辰,得到的內(nèi)容是說
Self-Sizing在iOS11下是默認開啟的奖恰,Headers, footers, and cells都默認開啟Self-Sizing宛裕,所有estimated 高度默認值從iOS11之前的 0 改變?yōu)?code>UITableViewAutomaticDimensionif #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 沒有太多的差異,只是注意局部刷新粒褒,會創(chuàng)建新的Cell
祥款。
下面兩篇文章也提到了類似的問題抠艾。
參考文章一
慎用局部刷新
因為之前對重用機制的理解存在誤區(qū),所以文章內(nèi)容更新了究履。