TableViewCell from Xib & MVVM

春麗 S.T.E.M.
14 min readAug 3, 2023

--

目錄

⦿ Xib
⦿ 基本設置(MVC)
⦿ Modify
⦿ MVVM

Xib

我們習慣使用 Storyboard 與程式碼搭配來建構 View,所以,在 Storyboard 中的一個 ViewController 上,加上一個 TableView 來去動態建構 Table 非常常見。

動態的 Table 常也需要客製化的 Cell,在 Storyboard 會見到如下:

左邊的 prototype cells 是另外放上 TableView(TableView 放在 ViewController 上) 的 UIElement,我們就可以客製化我們的 Cell,這是我們熟悉的建構動態 Table,並客製化 Cell 的方式。

這篇文章要使用 Xib 來客製化 Cell,即是右圖。

Xib 跟 Storyboard 同樣是一個圖形化介面,能讓使用者添加、設置可視的元件,而不是死氣沈沈的程式碼。

繼續閱讀|回目錄

基本設置(MVC)

首先,ViewController 裡加入 TableView,並使用 Cocoa Touch Class 創建一個客製化的 UITableViewCell,如下:

最終結果如右圖,我們希望在這個 Table 中呈現不同的動畫名,這個動畫使用不同的 Button 做為它的主要攻擊方式,例如咒術迴戰使用的是,鬼滅之刃使用的是,小丸子使用的是他的爺爺

如果將來加入更多資料,這個 Table 自然也會擴大。在 MVC 架構下,我們繼續來看看 View,還記得嗎?這裡使用 Xib 是為了客製化 Cell 如下:

左邊 Xib 檔案,為了要將 Cell 撐開(Cell 並沒有 intrinsic content size),左邊的 Label 在 top 及 bottom 都要設置約束條件,而不能使用在 superview 的 center(Vertical in Container),因為 Cell 並不是固定高度;而後方的 Button 在 Cell 確定高度後(因設置了 Label 的約束條件),就可以將之設為 superview 的 center 了。

而 Xib 的 UIElement 則要跟右邊的 Cocoa Touch Class File 用 IBOutlet 連接。

而要在 Cell 裡顯示資料,我們就需要一個 struct 如下:

struct AnimateData {
let title: String
let traillingTitle: String
}

這即是我們的 Model,接著看到 ViewController 的程式碼:

    @IBOutlet weak var animateTableView: UITableView!

private let cellItems: [AnimateData] = [
AnimateData(title: "咒術", traillingTitle: "術"),
AnimateData(title: "鬼滅", traillingTitle: "刀"),
AnimateData(title: "小丸子", traillingTitle: "爺")
]

override func viewDidLoad() {
super.viewDidLoad()
let nib = UINib(nibName: "\(AnimateTableViewCell.self)", bundle: nil)
animateTableView.register(nib, forCellReuseIdentifier: "\(AnimateTableViewCell.self)")

animateTableView.dataSource = self

}

我們當然也需要將 Storyboard 的 TableView 以 IBOutlet 連動,並且為了顯示資料,這個 TableView 的 dataSource(Delegation Pattern)必須設定為 self。

設定好 cellItems 以後,為了使用這個客製化的 Cell,我們必須創建 UINib 的實例,用的就是這個 Xib’s File name,接著,TableView 也要註冊這個 nib,同樣地,Cell 的 Identifier 也是 File name。

我們也看看下方的程式碼:

let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "\(ViewController.self)").")

第一行是不是跟創建 nib 很像呢?是的,第二行則同樣是實例的應用,必須要有 Storyboard(Xib)中的 Identifier。

最後,VC conform UITableViewDataSource 的程式碼如下:

extension ViewController: UITableViewDataSource {

func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
cellItems.count
}

func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(
withIdentifier: "\(AnimateTableViewCell.self)", for: indexPath)
as? AnimateTableViewCell else { return UITableViewCell() }

let item = cellItems[indexPath.row]
cell.titleLabel.text = item.title
cell.traillingBtn.setTitle(item.traillingTitle, for: .normal)

return cell

}

}

就完成了!

繼續閱讀|回目錄

Modify

但是!我們看看 ViewController 做了什麼事?

  1. 創建 Table 的顯示資料 cellItems
  2. 以 IBOutlet 連接 tableView,創建 nib 實例,tableView 註冊這個 nib(就能使用這個客製化 Cell)
  3. 設定 tableView 有多少 row
  4. 設定 cell 的顯示內容

除了第二點本來就是 ViewController 常做的事,其他的,你不會覺得 ViewController 管太多嗎?

首先,在這段程式碼中:

        let item = cellItems[indexPath.row]
cell.titleLabel.text = item.title
cell.traillingBtn.setTitle(item.traillingTitle, for: .normal)123

我們希望 Cell 能夠呼叫 configureUI 這樣的 function,藉由資料(Model)去建構自己的外觀(View),所以我們把邏輯放到客製化的 Cell 中,如下:

    func configureUI(title: String, traillingLabel: String) {
titleLabel.text = title
traillingBtn.setTitle(traillingLabel, for: .normal)
}

讓它自己設定外觀,所以 extension ViewController 就會變成如下:

let item = cellItems[indexPath.row]
cell.configureUI(title: item.title, traillingLabel: item.traillingTitle)

但這還不夠好,ViewController 需要知道 Cell 在設定 UI 時的細節嗎?其實不需要,所以我們再修改一次 Cell 的 configurUI 如下:

    func configureUI(with data: AnimateData) {
titleLabel.text = data.title
traillingBtn.setTitle(data.traillingTitle, for: .normal)
}

Cell 的資料是由 AnimateData 而來,所以 ViewController 就改成如下:

        let item = cellItems[indexPath.row]
cell.configureUI(with: item)

我們只要提供資料,並讓 Cell 把資料(Data)拿去建構它的外觀(View)就好,ViewController 不需要知道這麼多事。

這樣很棒吧?

我們已經把 Controller 跟 View 的邏輯分開,但這還差一點,我們仍必須在 Controller 中提供 Data,最終還是想把 Data 分開。

繼續閱讀|回目錄

MVVM

這即是說在蘋果的 MVC 架構下,我們還有 Data 從哪來的問題,如果 Data 總是由 Controller 提供,那麼,在 call API 後才取回的 Data,仍然要用 Controller 的 property 儲存,接著才放入想要呈現的 View 中,那 Controller 似乎工作又加重了。

雖然 call API 可以像這樣操作:

    private let registerService = RegisterService()

registerService.register(email: email,
password: password,
c_password: c_password) {
(result: Result<RegisterResponse, APIError>) in

switch result {
case .success(let response):

DispatchQueue.main.async {
if response.success {
let storyboard = UIStoryboard(name: .LoginOrRegister)
let vc = storyboard.instantiateVC(withClass: VerifyLinkSentViewController.self)
self.navigationController?.pushViewController(vc, animated: true)
self.data = response.data

} else {
self.showAlert(title: .caution,
message: response.message,
actionTitle: .confirm)
}
}

case .failure(let error):
let errorMsg = error.localizedDescription

DispatchQueue.main.async {
self.showAlert(title: .caution, message: errorMsg, actionTitle: .back)
}
}
}

在 call API 成功時,我們會得到 response,這個 response 也包括成功與否,這即是說 call API 成功是網路溝通的成功,而 response 是說這項操作成功與否,在操作成功時,我們才要取回想要的資料,否則只需要伺服器回應的訊息。

而網路成功,操作成功下,我們將回應的資料儲存,即是 self.data = response.data

Controller 好累喔,我們試著把資料放在一個新的概念上——ViewModel,既然叫做 ViewModel,就是專門負責 View 的 Model,看到下面程式碼:

class AnimateViewModel {

private let cellItems: [AnimateData] = [
AnimateData(title: "咒術", traillingTitle: "術"),
AnimateData(title: "鬼滅", traillingTitle: "刀"),
AnimateData(title: "小丸子", traillingTitle: "爺")
]

/// 項目數量
func numberOfItems() -> Int {
return cellItems.count
}

/// 項目索引
func itemAt(_ index: Int) -> AnimateData {
return cellItems[index]
}
}

把資料放在 ViewModel 裡,感覺好多了,由於 TableView 需要 row count,我們在這裡寫一個計算 cellItems 數量的 function;而建構 Cell 需要各個 item,我們把 cellItems 透過 index 拆開。

最後,在 ViewController 就可以改成如下:

    private let animateViewModel = AnimateViewModel()

extension ViewController: UITableViewDataSource {

func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
animateViewModel.numberOfItems()
}

func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(
withIdentifier: "\(AnimateTableViewCell.self)", for: indexPath)
as? AnimateTableViewCell else { return UITableViewCell() }

let item = animateViewModel.itemAt(indexPath.row)
cell.configureUI(with: item)
return cell

}


}

如此,我們就知道 ViewModel 的 Items count,就是 row count;item 就是 ViewModel 在 indexPath.row 這個 index 的資料(Data),透過這個資料(Data)就可以建構 Cell 的外觀(View)。

程式碼變得相當易懂吧?

AnimateTableViewCell(View)不知道 AnimateViewModel(ViewModel),AnimateViewModel 由 AnimateData(Model)建構;Controller 不知道 AnimateViewModel(ViewModel),也不知道 Cell(View),但知道 View 需由 ViewModel 建構 —— animateViewModel.numberOfItems()let item = animateViewModel.itemAt(indexPath.row)cell.configureUI(with: item)

這次就分享到這,感謝您的閱讀。

繼續閱讀|回目錄

附上 GitHub:

--

--

春麗 S.T.E.M.
春麗 S.T.E.M.

Written by 春麗 S.T.E.M.

Do not go gentle into that good night, Old age should burn and rave at close of day; Rage, rage, against the dying of the light.

No responses yet