28.%d Xcode — Simple example for MVVM Pattern & Data Binding
目錄
⦿ 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()
}
}
最終結果如下: