左邊的 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 做了什麼事?
- 創建 Table 的顯示資料 cellItems
- 以 IBOutlet 連接 tableView,創建 nib 實例,tableView 註冊這個 nib(就能使用這個客製化 Cell)
- 設定 tableView 有多少 row
- 設定 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: