講到 UITableView
,你們必定都不陌生。有一個相對誇張的說法,叫作學好 UITableView
,你就是一名合格的iOS
工程師swift
閒話少說,最近在寫 Swift
的過程當中碰到了如下幾個問題,特別在此記錄。數組
cellForRowAtIndexPath
代理中,對 cell
(尤爲是自定義cell
) 的初始化異同
OC
的區別 —— 不能使用OC
的那種判空方式來初始化dequeue
方法獲得的cell
永遠都是非空的,換言之,即使你自定義了一個初始化方法,它也不會被執行到。cell
的複用機制reloadData
時候,在iOS 11
上會產生抖動insertRow
和 deleteRow
和 reloadRows
同樣都屬於局部刷新的範疇,局部刷新時,系統會建立一個新的cell
來,並和舊的cell
在刷新時來回切換。setup
表示只會執行一次,並且在 cell 的初始化中表示他的繪圖(不帶數據)也只會執行一次render
表示渲染,其實是意味着setup
已經完成了繪圖,我要在每次重用時把數據傳進去渲染簡單的來講,tableview 的複用機制是咱們在 cellForRowAtIndexPath
的一系列操做。app
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 重疊的問題。google
根據上面所說的,Cell
的UI
在被建立後,就會被放進複用池中,等待被重用。可是若是像下面這種狀況:spa
一個TableView
中每一個Cell
的內容是根據數據中數組的個數來渲染的,就會出問題: 3d
Cell
分了不少層級,
除了頂部的 Header
區域是固定知道的高度外,下面的 區域 InfoA, InfoB, InfoC ...
等等,都是根據具體的信息去繪製的。 換言之,我不知道每一個 Cell 具體要畫幾個 InfoX
代理
這樣會形成一個很大的問題:
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)
}
}
複製代碼
下面是講解:
setupUI()
(只會執行一次)這個繪圖的工做作掉了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
。
由於以前對重用機制的理解存在誤區,因此文章內容更新了。