28.%d Xcode — Simple example for MVVM Pattern & Data Binding

春麗 S.T.E.M.
9 min readNov 30, 2021

--

目錄

⦿ MVC to MVVM
⦿ MVC
⦿ MVVM,View Model
⦿ MVVM,Cell
⦿ Data Binding

MVC to MVVM

MVC

我們對 Apple iOS 的 MVC pattern 相當熟悉了,如果今天 ViewController 中有大量的邏輯要處理,比方抓取 Data,將抓到的 Data 即時更新在 View 上,接著某些 UIElement 按下後,會有一些動作,例如編輯、換頁⋯⋯。

我的天哪!ViewController 也做太多事了吧,我們試著將抓取 Data(Model) 的動作拉出來,給一個叫做 ViewModel 的東西來做,接著,如果它是 TableViewCell 要呈現的資料,就會用到 indexPath(通常是 indexPath.row),也就是抓取回來的資料,我們希望能夠把它分離成一個個的 item,分別給 Cell Row呈現。

今天假設有一個 TableView,如下:

要呈現這樣一個表格(List),我們會用到 TableView 或 TableViewController,而表格的資料可能是本地獲取,可能是網路獲取,如果你今天在專案裡放了一個 json file,我們就會在程式碼裡讀取這個 file,例如在 ViewController 裡,去找到 json 這個 Extension File(副檔名),那麼檔案名稱就是 Bundle 裡的檔名,例如 Zodiac。

所以 Fetch Data 就可能會在 viewDidLoad 中做,如下:

var zodiacs = [ZodiacData]()guard let url = Bundle.main.url(forResource: "Zodiac", withExtension: "json") else {
print("Can't find this url.")
return
}

do {
let decoder = JSONDecoder()
let data = try Data(contentsOf: url)
let zodiacs = try decoder.decode([ZodiacData].self,
from: data)
self.zodiacs = zodiacs
}
catch {
print("Can't get JsonData from the url.")
}

但此時此刻,我們已經不想要在 ViewController 裡幫忙建構 Cell(View)的資料了,所以在 MVC Architecture 下,setupCell 就會寫在客製化的 Cell 裡,最後再去呼叫它,如下:

如此一來,extension ViewController: UITableViewDataSource 就會變得較好看簡潔,不過這張圖給你看的是 ViewModel 去做,最後變成一行,實際上若在 ViewController 中 Fetch Data 實際操作如下:

// numberOfRowsInSection
self.zodiacs.count

// cellForRowAt
let item = zodiacs[indexPath.row]
cell.setupCell(data: item)

看起來沒有差很多,但重點其實不是程式碼的多寡,而是邏輯的分佈,將來在維護上,如果出錯是否好測試、好追蹤。

MVVM,View Model

如果看到 MVVM 的資料夾整理術,會長成下面這樣:

當然架構的改變不是為了資料夾整理術,資料夾整理只是附加價值,我們把 ViewController Fetch Data 改在 View Model 中做,而 ViewController 只要產生 ViewModel,再由 ViewModel 去 Fetch Data 即可。

先看到最終的 ViewController:

再看到 ViewModel:

與 VC 同,需要一個 Property 去接值,當呼叫 fetchData 時,將得到的結果裝到 Property 裡。

MVVM,Cell

最後看到 Cell:

在 MVC 中,setupCell with data: ZodiacData 這樣即可,今天 MVVM 希望帶入 setupCell 的值是從 View Model 而來,還記得剛才的 View Model 裡,zodiacs 就是 [ZodiacData] 這樣的形式。

我們可以想像得到 View Model 取得 Data(Model),View Model 知道 Data(Model),但是 Cell 不該知道 Data(Model),所以 setupCell with viewModel: ListCellViewModel 勢在必行。

class ListTableViewCell: UITableViewCell {

func setupCell(with viewModel: ListCellViewModel, at index: Int) {

}
}

由於最終要拿到的是一個個 item(Data(Model)),所以必須要有 index,而在 ViewModel 中可以簡化 index 的流程為:

class ListCellViewModel: CellViewModelInterface {

var cellItems = [ZodiacData]()

func itemAt(_ index: Int) -> ZodiacData {
return cellItems[index]
}

}

就可以回頭寫 setupCell,如下:

func setupCell(with viewModel: ListCellViewModel, at index: Int) {
let item = viewModel.cellItems[index]
leadingLabel.text = item.name
trailingLabel.text = item.tillDate
}

那麼,在 ViewController 裡就會這樣使用:


private var listViewModel = ListCellViewModel()

override func viewDidLoad() {
super.viewDidLoad()
listViewModel.fetchData()
}

extension ViewController: UITableViewDataSource {

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

func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {

guard let cell = tableView.dequeueReusableCell(withIdentifier: "\(ListTableViewCell.self)", for: indexPath) as? ListTableViewCell else {
return UITableViewCell()
}

cell.setupCell(with: listViewModel, at: indexPath.row)

return cell
}

}

Cool!變得相當乾淨整齊了。

繼續閱讀|回目錄

Data Binding

同樣地,當我們學過 Boxing,可將之運用在 Data(Model)中,既然取回檔案的是 View Model,我們就會這樣宣告:

var cellItems = Box([ZodiacData]())

而你只要知道 cellItems 的 value 此時是 [ZodiacData] 即可。但是,Data 是要跟哪個 UI 做 Binding 呢?

當這個 Data 更新時,我們希望 TableView 更新,所以就會這樣寫:

override func viewDidLoad() {
super.viewDidLoad()

listViewModel.fetchData()
listViewModel.cellItems.bind { _ in
self.listTableView.reloadData()
}
}

最終結果如下:

--

--

春麗 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