Swift 踩坑筆記 —— UITableView Cell初始化和重新整理的問題探討

黑羽肅霜_發表於2018-09-13

綜述

講到 UITableView,大家一定都不陌生。有一個相對誇張的說法,叫做學好 UITableView,你就是一名合格的iOS 工程師

閒話少說,最近在寫 Swift 的過程中碰到了以下幾個問題,特別在此記錄。

遇到的問題

  • cellForRowAtIndexPath 代理中,對 cell(尤其是自定義cell) 的初始化異同
    • OC的區別 —— 不能使用OC的那種判空方式來初始化
    • 初始化不能使用自定義的方法 —— 通過dequeue方法得到的cell 永遠都是非空的,換言之,即便你自定義了一個初始化方法,它也不會被執行到。
    • 通過渲染方式(render)來繪製影象,賦值
    • 理解cell的複用機制
  • 重新整理的問題
    • 使用 reloadData時候,在iOS 11 上會產生抖動
    • insertRowdeleteRowreloadRows 一樣都屬於區域性重新整理的範疇,區域性重新整理時,系統會建立一個新的cell來,並和舊的cell在重新整理時來回切換。

先明確幾個概念

  • 程式碼中的 setup 表示只會執行一次,而且在 cell 的初始化中表示他的繪圖(不帶資料)也只會執行一次
  • 程式碼中的render 表示渲染,實際上是意味著setup已經完成了繪圖,我要在每次重用時把資料傳進去渲染

重申 Cell 的複用機制和使用

簡單的來說,tableview 的複用機制是我們在 cellForRowAtIndexPath 的一系列操作。

  • CellUI 一旦被建立,系統就會存放在複用池中等待複用。
  • Cell 的可變內容(通常是labeltextimage的內容,選中的背景色等),是不會記錄的。
  • 刪除某個 Cell 後再建立一個新的 Cell, 實際上你會發現新的 Cell 中有部分 UI 時舊 Cell中的
  • reloadRows 區域性重新整理時會建立新的 Cell,再重新整理時會和舊的Cell來回切換

很簡單的情況是,如果我們不每次滾動的時候去dataSource陣列中把對應index的數值取出來,只管的感受就是UI雖然固定,但是資料和圖片一直在亂跑

鑑於Swift 無法自定義cell的初始化,那麼上下滾動時,怎麼重新賦值而不重複繪製就顯得格外重要。

關於 cellForRowAtIndexPath 的初始化問題其實在這篇文章中已經討論過,這裡不作贅述 Swift 踩坑筆記(二)—— 初始化Tableview 及自定義 TableviewCell

我們要討論的是在Cell複用過程中的賦值和 UI 重疊的問題。

典型案例 —— Cell 的 UI 內容根據資料而定

描述

根據上面所說的,CellUI 在被建立後,就會被放進複用池中,等待被重用。但是如果像下面這種情況:

一個TableView 中每個Cell 的內容是根據資料中陣列的個數來渲染的,就會出問題:

image.png
我們這裡的 Cell 分了很多層級,

除了頂部的 Header區域是固定知道的高度外,下面的 區域 InfoA, InfoB, InfoC ...等等,都是根據具體的資訊去繪製的。 換言之,我不知道每個 Cell 具體要畫幾個 InfoX

這樣會造成一個很大的問題:

  • 因為根據複用機制,資料是每次都有可能不同的,而根據資料建立的 UI 一旦被建立,就會一直存在於複用池中。
  • 如果 Cell 發生了刪除,再新增,就有可能將那些不用的Cell UI 複用進來。
  • 區域性重新整理時會建立新的 Cell,這時候疊加在舊的UI上切換時,就會造成檢視的重疊

來看下錯誤的現象圖

區域性重新整理的效果

區域性重新整理的效果.gif

使用 reveal 檢視,發現多了一個層級UI,蓋在應該有的位置()

image.png

正確的程式碼

為了避免混淆,我這裡就不貼原來錯誤的程式碼了。

來看下面正確的程式碼

// 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,先把這幾個 subViewremoveFromSuperView 避免干擾,然後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的缺點

  • 效能問題 我們都知道,UITableviewreloadData 是需要慎用的。因為他會將整個tableview 都重新整理一遍。這意味著也許我只需要重新整理2個cell,你卻讓所有的cell都重渲染了一遍。從效能而言這顯然是不可取的。 所以我們才會想到去用區域性重新整理。

  • reloadData 無法像系統提供的其他重新整理方法一樣,帶有animate引數,這讓重新整理時,整個頁面看起來非常突兀。如果你不自己加動畫,那麼體驗真的不太好

  • iOS 11 上會有一個問題,就是過載之後頁面會亂跑:

    頁面亂跑.gif

    • 解決辦法: 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

下面兩篇文章也提到了類似的問題。 參考文章一 慎用區域性重新整理


因為之前對重用機制的理解存在誤區,所以文章內容更新了。

相關文章