綜述
講到 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
。
下面兩篇文章也提到了類似的問題。 參考文章一 慎用區域性重新整理
因為之前對重用機制的理解存在誤區,所以文章內容更新了。