iOS App 程式開發

利用 Swift Generic 建置可重複使用的 UITableViewController!

當我們想要在 UITableViewController 中顯示數據的類型越多,就有可能導致重複和維護困難。透過 Swift Generic 來創建簡單抽象,建置可重用的 Generic UITableViewController,就可以解決這個問題了。
利用 Swift Generic 建置可重複使用的 UITableViewController!
利用 Swift Generic 建置可重複使用的 UITableViewController!
In: iOS App 程式開發, Swift 程式語言
本篇原文(標題:Building Reusable Generic UITableViewController in iOS App)刊登於作者Medium,由 Alfian Losari 所著,並授權翻譯及轉載。

TableView Controller 是一個不可或缺的 UIKit 元件,幾乎每個 iOS App 都會用到它來顯示列表中的數據集合。當我們想要在 UITableViewController 中顯示不同類型的數據時,通常都會創建一個新的子類來顯示相關類型的數據。這種方法可行沒錯,但如果我們的 App 中有許多不同類型的數據,則可能導致重複和維護困難。

我們如何處理和解決這個問題?其中一種方法是利用簡單抽象,透過 Swift Generic Abstract Data Type 來創建 Generic (泛型) UITableViewController 子類,這個子類可以使用 Swift Generic Constraint 配置和顯示不同類型的數據。

讀者可以在GitHub repository 中找到並構建初始專案。

建置泛型 TableViewController

我們創建一個 UITableViewController 的子類,命名為 GenericTableViewController ,並添加 2 種泛型 TCell。在此,我們添加了一個限制,讓 Cell 必須是 UITableViewCell 的子類。T 將用作數據的抽象,而 Cell 將被註冊到 UITableView,並從隊列取出資料,以 UITableViewCell 將每行的數據顯示出來。

class GenericTableViewController<T, Cell: UITableViewCell>: UITableViewController {
  var items: [T]
  var configure: (Cell, T) -> Void
  var selectHandler: (T) -> Void
  init(items: [T], configure: @escaping (Cell, T) -> Void, selectHandler: @escaping (T) -> Void) {
    self.items = items
    self.configure = configure
    self.selectHandler = selectHandler
    super.init(style: .plain)
    self.tableView.register(Cell.self, forCellReuseIdentifier: "Cell")
  }
  ...
}

我們來看看初始化函式,它接受 3 個參數:

  1. 泛型 T 的陣列:這將被指定為驅動 UITableViewDataSource 的實例變數。
  2. 配置閉包 (closure):當 tableview 從隊列取出資料在每行 cell 中顯示時,將調用此配置閉包,傳遞 T data 和 Cell。在這裡,我們會設置如何用數據顯示 UITableViewCell。 (只要 CellUITableViewCell 的子類,我們就可以在參數中明確聲明 Cell 的類型,來讓編譯器能夠隱式推斷Cell的類型。)
  3. Selected Handler 閉包:當用戶選擇/輕擊 cell 時,將調用此閉包。在這裡,我們可以添加當用戶點擊時將調用的邏輯或操作。

初始化函式將 3 個參數各自分配至該類別的實體變量中,然後將 Cell 註冊為具有可重用標識符 (reusable identifier) 的 UITableView,用於取出 UITableViewCell 的數據源。

class GenericTableViewController<T, Cell: UITableViewCell>: UITableViewController {
  ....
  //1
  override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return items.count
  }
  //2  
  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! Cell
    let item = items[indexPath.row]
    configure(cell, item)
    return cell
  }
  //3
  override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    tableView.deselectRow(at: indexPath, animated: true)
    let item = items[indexPath.row]
    selectHandler(item)
  }
}

以下是我們需要覆寫 (override) 的 UITableViewDataSourceUITableViewDelegate 方法:

  1. tableView:numberOfRowsInSection::這裡我們只回傳 T 物件陣列中的數據數
  2. tableView:cellForRowAtIndexPath::我們利用可重用標識符來取出 UITableViewCell,並將其轉換為 Cell。然後,我們使用 index path row 從 T 陣列中獲取數據。之後,我們調用配置閉包傳遞這個 cell 與剛才獲取的 data,以在顯示前客製化數據。
  3. tableView:didSelectRowAtIndexPath::這裡我們只使用 index path row 從陣列中獲取數據,並調用 selectHandler 閉包傳遞數據。

使用 GenericTableViewController

為了讓不同類型的物件嘗試使用 GenericTableViewController,我們創建了兩個簡單的 struct:PersonFilm。在每個 struct 中,我們創建一個靜態計算變數 (static computed variable),它將為每個 struct 回傳一個寫死 (hardcoded) 的物件陣列。

struct Person {
  
  let name: String
  
  static var stubPerson: [Person] {
    return [
      Person(name: "Mark Hamill"),
      Person(name: "Harrison Ford"),
      Person(name: "Carrie Fisher"),
      Person(name: "Hayden Christensen"),
      Person(name: "Ewan McGregor"),
      Person(name: "Natalie Portman"),
      Person(name: "Liam Neeson")
   ]
  }
}
struct Film {
  let title: String
  let releaseYear: Int
  static var stubFilms: [Film] {
    return [
      Film(title: "Star Wars: A New Hope", releaseYear: 1978),
      Film(title: "Star Wars: Empire Strikes Back", releaseYear: 1982),
      Film(title: "Star Wars: Return of the Jedi", releaseYear:  1984),
      Film(title: "Star Wars: The Phantom Menace", releaseYear: 1999),
      Film(title: "Star Wars: Clone Wars", releaseYear: 2003),
      Film(title: "Star Wars: Revenge of the Sith", releaseYear: 2005)]
  }
}

設置 Person GenericTableViewController

let personsVC = GenericTableViewController(items: Person.stubPerson, configure: { (cell: UITableViewCell, person) in
  cell.textLabel?.text = person.name
}) { (person) in
  print(person.name)
}

我們將使用基本 UITableViewCell Basic style 顯示 Person 列表。在這裡,我們實例化 GenericTableViewController 來傳遞 Person 物件的陣列。閉包使用基本 UITableViewCell 作為 Cell 的類型,在配置中我們只使用 person 的名稱分配 textLabel 文本屬性。在 selected handler 閉包中,我們只需將所選 person 的名稱打印到控制台即可。你可以在這裡看到 Swift 隱式類型引用的強大功能,編譯器會自動將 T 泛型替換為 Person struct。

設置 Film GenericTableViewController

Setting Up the Film GenericTableViewController
class SubtitleTableViewCell: UITableViewCell {
  override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
    super.init(style: .subtitle, reuseIdentifier: nil)
  }
  ...
}

對於 Film,我們將使用帶有 Subtitle 樣式的 UITableViewCell 來顯示它。為了能夠做到這一點,我們需要創建子類來覆寫預設樣式,以使用我們稱為 SubtitleTableViewCell 的 Subtitle 樣式。

let filmsVC = GenericTableViewController(items: Film.stubFilms, configure: { (cell: SubtitleTableViewCell, film) in
  cell.textLabel?.text = film.title
  cell.detailTextLabel?.text = "\(film.releaseYear)"
}) { (film) in
  print(film.title)
}

我們實例化 GenericTableViewController 來傳遞 Film 物件的陣列。對於配置閉包,我們將 Cell 參數的 cell type 設置為 SubtitleTableViewCell,然後在閉包內部,我們就使用電影標題 (film title) 和發行年份 (release year),來設置 cell 的 textLabeldetailTextLabel 文本屬性。而在 selected handler 閉包中,我們只將所選電影的標題打印到控制台。

使用 UITabBarController 作為 Container View Controller 作最終整合

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?
  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Instantiate person and film table view controller
    ...
    let tabVC = UITabBarController(nibName: nil, bundle: nil)
    tabVC.setViewControllers([
      UINavigationController(rootViewController: personsVC),
      UINavigationController(rootViewController: filmsVC)
    ], animated: false)
    
    window = UIWindow(frame: UIScreen.main.bounds)
    window?.rootViewController = tabVC
    window?.makeKeyAndVisible()
   
    return true
  }
}

要在 iOS 專案中顯示它,我們可以使用包含了 GenericTableViewControllerPersonFilm 實例的 UITabBarController 作為 ViewController。我們將 tab bar controller 設置為 UIWindow root ViewController ,並將每個泛型 table view controller 嵌入到 UINavigationController 中。

結論

我們終於成功使用 Swift 泛型為 UITableViewController 創建一個 abstract Container 類別。這個方法真的能夠幫助我們對不同類型的數據源重用相同的 UITableViewController,而且我們仍然可以使用符合 UITableViewCell 的泛型 Cell 來客製, Swift 泛型是一個非常神奇的範例,可以用來創建一個非常強大的抽象。

本篇原文(標題:Building Reusable Generic UITableViewController in iOS App)刊登於作者Medium,由 Alfian Losari 所著,並授權翻譯及轉載。
作者簡介:Alfian Losari 是一位軟體工程師和 Swift 愛好者,他非常熱愛一切關係科技及其價值的事。他的座右銘是:如果人停止學習,就永遠不會進步。
譯者簡介:陳奕先-過去為平面財經記者,專跑產業新聞,2015 年起跨進軟體開發世界,希望在不同領域中培養新的視野,於新創學校 ALPHA Camp 畢業後,積極投入 iOS 程式開發,目前任職於國內電商公司。聯絡方式:電郵 [email protected]

FB : https://www.facebook.com/yishen.chen.54
Twitter : https://twitter.com/YeEeEsS

作者
AppCoda 編輯團隊
此文章為客座或轉載文章,由作者授權刊登,AppCoda編輯團隊編輯。有關文章詳情,請參考文首或文末的簡介。
評論
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。