TableView Controller 是一個不可或缺的 UIKit 元件,幾乎每個 iOS App 都會用到它來顯示列表中的數據集合。當我們想要在 UITableViewController
中顯示不同類型的數據時,通常都會創建一個新的子類來顯示相關類型的數據。這種方法可行沒錯,但如果我們的 App 中有許多不同類型的數據,則可能導致重複和維護困難。
我們如何處理和解決這個問題?其中一種方法是利用簡單抽象,透過 Swift Generic Abstract Data Type 來創建 Generic (泛型) UITableViewController
子類,這個子類可以使用 Swift Generic Constraint 配置和顯示不同類型的數據。
讀者可以在GitHub repository 中找到並構建初始專案。
建置泛型 TableViewController
我們創建一個 UITableViewController
的子類,命名為 GenericTableViewController
,並添加 2 種泛型 T
和 Cell
。在此,我們添加了一個限制,讓 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 個參數:
- 泛型
T
的陣列:這將被指定為驅動UITableViewDataSource
的實例變數。 - 配置閉包 (closure):當 tableview 從隊列取出資料在每行 cell 中顯示時,將調用此配置閉包,傳遞
T
data 和Cell
。在這裡,我們會設置如何用數據顯示UITableViewCell
。 (只要Cell
是UITableViewCell
的子類,我們就可以在參數中明確聲明 Cell 的類型,來讓編譯器能夠隱式推斷Cell
的類型。) - 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) 的 UITableViewDataSource
和 UITableViewDelegate
方法:
tableView:numberOfRowsInSection:
:這裡我們只回傳T
物件陣列中的數據數tableView:cellForRowAtIndexPath:
:我們利用可重用標識符來取出UITableViewCell
,並將其轉換為Cell
。然後,我們使用 index path row 從T
陣列中獲取數據。之後,我們調用配置閉包傳遞這個 cell 與剛才獲取的 data,以在顯示前客製化數據。tableView:didSelectRowAtIndexPath:
:這裡我們只使用 index path row 從陣列中獲取數據,並調用 selectHandler 閉包傳遞數據。
使用 GenericTableViewController
為了讓不同類型的物件嘗試使用 GenericTableViewController
,我們創建了兩個簡單的 struct:Person
和 Film
。在每個 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 的 textLabel
和 detailTextLabel
文本屬性。而在 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 專案中顯示它,我們可以使用包含了 GenericTableViewController
的 Person
和 Film
實例的 UITabBarController
作為 ViewController。我們將 tab bar controller 設置為 UIWindow
root ViewController ,並將每個泛型 table view controller 嵌入到 UINavigationController
中。
結論
我們終於成功使用 Swift 泛型為 UITableViewController
創建一個 abstract
Container 類別。這個方法真的能夠幫助我們對不同類型的數據源重用相同的 UITableViewController
,而且我們仍然可以使用符合 UITableViewCell
的泛型 Cell
來客製, Swift 泛型是一個非常神奇的範例,可以用來創建一個非常強大的抽象。
FB : https://www.facebook.com/yishen.chen.54
Twitter : https://twitter.com/YeEeEsS