【譯】如何合理地處理複雜TableView頁面

原文連接:medium.cobeisfresh.com/dealing-wit… 求大佬們點個關注,會按期寫原創和翻譯國外最新文章,跟大佬們一塊兒學習進步,有問題或者建議歡迎加微信ruiwendelll,拉你們進技術交流羣,一塊兒探討學習,謝謝了!react

table view是iOS開發中最重要的佈局組件之一。一般咱們最重要的一些頁面是表格視圖:Feed,設置,列表等。ios

每一個寫過複雜table viewiOS開發人員都知道它能夠很是快速地實現。它有大量的UITableViewDataSource方法和大量的if和switch語句。編程

我總結了一套原則,我暫時滿意,這有助於我克服這些問題。這些技巧的好處在於它們不只適用於複雜的表視圖,並且也適用於全部表視圖。後端

下面是一個複雜table view的例子:數組

這是PokeBall,Pokémon的社交網絡。與全部社交網絡同樣,它須要一個顯與用戶的不一樣動態的Feed。這些動態包括按天分組的新照片和狀態消息。所以,咱們有兩個點須要擔憂:表視圖具備不一樣的狀態,以及多個cell和section。安全

Cell

我看到不少開發者將cell配置過程放在他們的cellForRowAt:方法中。w咱們思考一下啊,該方法的目的是建立一個cell。 UITableViewDataSource的目的是提供數據。dataSource不該該爲按鈕設置字體。bash

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(
    withIdentifier: identifier,
    for: indexPath) as! StatusTableViewCell
  
  let status = statuses[indexPath.row]
  cell.statusLabel.text = status.text
  cell.usernameLabel.text = status.user.name
  
  cell.statusLabel.font = .boldSystemFont(ofSize: 16)
  return cell
}
複製代碼

你應該把樣式相關的的代碼放在cell內部。若是它在cell的整個生命週期中都會出現,就像標label的字體同樣,將它放在awakeFromNib方法中。微信

class StatusTableViewCell: UITableViewCell {
  
  @IBOutlet weak var statusLabel: UILabel!
  @IBOutlet weak var usernameLabel: UILabel!
  
  override func awakeFromNib() {
    super.awakeFromNib()
    
    statusLabel.font = .boldSystemFont(ofSize: 16)
  }
}
複製代碼

除此以外,你可使用屬性觀察器來設置cell的數據。網絡

var status: Status! {
  didSet {
    statusLabel.text = status.text
    usernameLabel.text = status.user.name
  }
}
複製代碼

這樣你的cellForRow方法就清晰可靠多了。數據結構

func tableView(_ tableView: UITableView, 
  cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(
    withIdentifier: identifier,
    for: indexPath) as! StatusTableViewCell
  cell.status = statuses[indexPath.row]
  return cell
}
複製代碼

更重要的是,特定於cell的邏輯如今位於一個單獨的位置,而不是分散在cell和視圖控制器之間。

Model

一般,你使用從某種後端接口得到的模型對象數組來填充table view。而後,cell須要根據該模型對自身進行更改。

var status: Status! {
  didSet {
    statusLabel.text = status.text
    usernameLabel.text = status.user.name
    
    if status.comments.isEmpty {
      commentIconImageView.image = UIImage(named: "no-comment")
    } else {
      commentIconImageView.image = UIImage(named: "comment-icon")
    }
    
    if status.isFavorite {
      favoriteButton.setTitle("Unfavorite", for: .normal)
    } else {
      favoriteButton.setTitle("Favorite", for: .normal)
    }
  }
}
複製代碼

您能夠建立一個特定於單元格的模型,您將使用模型對象進行初始化,它將處理cell的標題,圖像和其餘屬性。

class StatusCellModel {
  
  let commentIcon: UIImage
  let favoriteButtonTitle: String
  let statusText: String
  let usernameText: String
  
  init(_ status: Status) {
    statusText = status.text
    usernameText = status.user.name
    
    if status.comments.isEmpty {
      commentIcon = UIImage(named: "no-comments-icon")!
    } else {
      commentIcon = UIImage(named: "comments-icon")!
    }
    
    favoriteButtonTitle = status.isFavorite ? "Unfavorite" : "Favorite"
  }
}
複製代碼

如今,您能夠將大量cell的展現邏輯移動到model。而後,您能夠單獨實例化和單元測試model,而無需在單元測試中進行復雜的模擬和獲取單元格。這也意味着您的cell代碼很是簡單易讀。

var model: StatusCellModel! {
  didSet {
    statusLabel.text = model.statusText
    usernameLabel.text = model.usernameText
    commentIconImageView.image = model.commentIcon
    favoriteButton.setTitle(model.favoriteButtonTitle, for: .normal)
  }
}
複製代碼

這是與MVVM相似的模式,但應用於單個table view cell。

矩陣

分section的table view一般會早成代碼很亂。你見過相似下面的代碼嗎:

func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
  switch section {
  case 0: return "Today"
  case 1: return "Yesterday"
  default: return nil
  }
}
複製代碼

這是不少代碼,而且不少硬編碼索引應該很是簡單,易於更改和交換。這個問題有一個簡單的解決方案:矩陣。

還記得矩陣嗎?這是機器學習相關開發者和一年級CS專業學生使用的東西,但應用程序開發人員一般不這樣作。然而,若是你想到一個分段的table view,你正在展現一個section列表。每一個section都是一個cell列表。這聽起來像一個數組或矩陣。

這就是你應該對分段table view進行建模的方式。而不是一維數組,使用二維數組。這就是UITableViewDataSource方法的結構:你被要求返回第m個section的第n個cell,而不是table View自己的第n個cell。

var cells: [[Status]] = [[]]
  
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(
    withIdentifier: identifier,
    for: indexPath) as! StatusTableViewCell
  cell.status = statuses[indexPath.section][indexPath.row]
  return cell
}
複製代碼

而後咱們能夠經過定義Section容器類型來擴展這個概念。此類型不只會保存某個section的cell,還會保留secton標題。

struct Section {
  let title: String
  let cells: [Status]
}
var sections: [Section] = []
複製代碼

如今咱們能夠避免使用咱們的硬編碼索引,而是能夠定義一個section數組並直接返回它們的標題。

func tableView(_ tableView: UITableView, 
  titleForHeaderInSection section: Int) -> String? {
  return sections[section].title
}
複製代碼

這樣,咱們的數據源方法中的代碼就越少,所以越界錯誤的可能性就越小。代碼也變得更具表現力和可讀性。

枚舉

使用多種cell類型可能很是棘手。考慮某種類型的feed,你必須顯現不一樣類型的cell,如照片和狀態。爲了保持清楚並避免奇怪的數組索引運算,你應該將它們存儲在同一個數組中。

可是,數組是同質的,這意味着您不能擁有不一樣類型的數組。想到的第一個解決方案是協議。畢竟,Swift是面向協議的!

你能夠定義協議FeedItem,並確保咱們的cell的模型實現該協議。

protocol FeedItem {}
struct Status: FeedItem { ... }
struct Photo: FeedItem { ... }
複製代碼

而後你能夠定義一個FeedItem數組

var cells: [FeedItem] = []
複製代碼

可是,在實現cellForRowAt時:使用此解決方案,咱們能夠看到一個小問題。

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  let cellModel = cells[indexPath.row]
  
  if let model = cellModel as? Status {
    let cell = ...
    return cell
  } else if let model = cellModel as? Photo {
    let cell = ...
    return cell
  } else {
    fatalError()
  }
}
複製代碼

在將模型向上轉換爲協議時,您丟失了許多實際須要的信息。你已經抽出了cell,但實際上你須要具體的實例。所以,您最終必須檢查是否能夠轉換爲類型,而後根據該類型顯示cell。

這會有效,但它並不漂亮。向下傾斜本質上是不安全的,並致使optional。你也不知道是否已涵蓋全部 狀況,由於無數種類型均可以實現你的協議。這就是爲何你須要調用fatalError,爲了防止你獲得一個意外的類型。

當您嘗試將協議的實例強制轉換爲具體類型時,一般會使代碼出現問題。當你不須要特定信息時,可使用協議,但可使用原始數據的子集代替。

更好的方法是使用枚舉。這樣你能夠打開它,若是你沒有處理全部狀況,代碼將沒法編譯。

enum FeedItem {
  case status(Status)
  case photo(Photo)
}
複製代碼

枚舉也能夠有關聯的值,所以您能夠將所需的數據放在特定的枚舉值中。

你的數組定義保持不變,但你的cellForRowAt:方法如今看起來更清晰:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  let cellModel = cells[indexPath.row]
  
  switch cellModel {
  case .status(let status):
    let cell = ... 
    return cell
  case .photo(let photo):
    let cell = ...
    return cell
  }
}
複製代碼

這樣,你沒有強制轉換,沒有可選項和沒有未處理的狀況,因此咱們沒有錯誤。

使狀態清晰

由於看到空白屏幕會讓人感到困惑,因此當table view爲空時,咱們一般會顯示某種消息。咱們還在數據加載時顯示一個加載動畫。可是,若是事情不對,那麼告訴用戶發生了什麼以便他們知道如何解決問題會很好。

咱們的table view一般具備全部這些狀態等等。管理它們可能會很痛苦

假設您有兩種可能的狀態:顯示數據或無數據視圖。一個naive的開發人員會隱藏table View並展現無數據視圖就來表示「無數據」狀態。

noDataView.isHidden = false
tableView.isHidden = true
複製代碼

在這種狀況下更改狀態意味着您必須更改兩個bool屬性。在視圖控制器的另外一部分中,您可能但願將狀態設置爲其餘部分,而且須要記住設置兩個屬性。

實際上,這兩個bool屬性應該始終保持同步。您不能擁有無數據視圖而同時以顯示一些數據。

考慮現實世界狀態數與應用中可能的狀態數之間的區別頗有用。兩個布爾值有四種可能的組合。這意味着你有兩個不想要的無效狀態,你須要處理這些狀態。

您能夠經過定義一個包含屏幕可能處於的全部可能狀態的狀態枚舉來解決此問題。

enum State {
  case noData
  case loaded
}
var state: State = .noData
複製代碼

你還能夠定義單個狀態屬性,這是更改屏幕狀態的惟一方法。每次更改屬性時,你都將更新屏幕以顯示該狀態。

var state: State = .noData {
  didSet {
    switch state {
    case .noData:
      noDataView.isHidden = false
      tableView.isHidden = true
    case .loaded:
      noDataView.isHidden = false
      tableView.isHidden = true
    }
  }
}
複製代碼

若是你只經過此屬性修改狀態,則能夠確保永遠不會忘記更新屬性,而且永遠不會輸入無效狀態。如今改變狀態很簡單。

self.state = .noData
複製代碼

你擁有的狀態越多,此模式就越有用。

你甚至能夠經過使用咱們的錯誤消息和項目的關聯值來改善這一點。

enum State {
  case noData
  case loaded([Cell])
  case error(String)
}
var state: State = .noData {
  didSet {
    switch state {
    case .noData:
      noDataView.isHidden = false
      tableView.isHidden = true
      errorView.isHidden = true
    case .loaded(let cells):
      self.cells = cells
      noDataView.isHidden = true
      tableView.isHidden = false
      errorView.isHidden = true
    case .error(let error):
      errorView.errorLabel.text = error      
      noDataView.isHidden = true
      tableView.isHidden = true
      errorView.isHidden = false
    }
  }
}
複製代碼

這樣,你就定義了一個數據結構,它是table view controller的完整表示。它很容易測試(由於它是一個純粹的Swift值),併爲咱們的table view提供單點更新和單一事實來源。

建議

下面是幾個小貼士,很是有用:

reactive

確保table view始終展現數據源源數組的當前狀態。使用屬性觀察器刷新table View,不要嘗試手動保持它們同步。

Delegate != ViewController

任何人均可以實現協議!請記住,下次編寫複雜的table view數據源或委託時。定一個惟一目的是table view的數據源的類型會更好 。這樣能夠保持視圖控制器的乾淨,並將邏輯和職責分離到各自的對象中。

不要比較index

若是你發現你會確認某個indexPath是某個確切的index,經過switch語句到某個section,或者相似的操做。這是不對的。若是你有某個cell要放在肯定的位置,在你的源數組中展現它。不要在你的代碼中隱藏這些cell。

記住法則

總而言之,惟一的法則是在編程中,朋友只和它的朋友交談,不要和朋友的朋友交談。

換句話說,一個對象應該只訪問它本身的屬性。那些屬性的屬性應該保持不變。因此,UITableViewDataSource不該該爲cell的label設置text屬性。若是你在代碼中看到兩個點(例如cell.label.text=...)那就是不對的。

若是你不按照這個原則來,更改cell意味着你也不得不更改數據源。將cell和數據源解耦可讓你更改或者重構一個cell而不用影響其餘。

錯誤的抽象

有時候,擁有多個相似的UITableViewCell類比使用一堆if語句的單個類更好。你不會知道他們之後會怎麼出現問題,將它們抽象是一個陷阱。

我但願這些技巧能夠幫助你,我相信你下次寫table view相關代碼會用到這些建議。

求大佬們點個關注,會按期寫原創和翻譯國外最新文章,跟大佬們一塊兒學習進步,有問題或者建議歡迎加微信ruiwendelll,拉你們進技術交流羣,一塊兒探討學習,謝謝了!

相關文章
相關標籤/搜索