原創(chuàng)作者:Paul Hudson
原文鏈接:How to move data sources and delegates out of your view controllers
這是解決 “臃腫的ViewController” 這個問題系列教程中的第二部分:
- 如何在 iOS app 中使用協(xié)調(diào)模式
- 如何把數(shù)據(jù)源和代理從你的 ViewController 抽離出來
- 如何把關(guān)于視圖構(gòu)建的代碼搬離 ViewController
創(chuàng)建混亂和可讀性差的 ViewController 的最簡單方法之一是忽略單一責(zé)任原則,即程序中的每個部分一次只負(fù)責(zé)一件事关带。
忽略這一原則的一個比較有代表性的情況是編寫如下代碼:
class MegaController: UIViewController, UITableViewDataSource, UITableViewDelegate, UIPickerViewDataSource, UIPickerViewDelegate, UITextFieldDelegate, WKNavigationDelegate, URLSessionDownloadDelegate {
}
如果我問你上面那個ViewController是做什么的,你能夠一口氣把它說完嗎???
我不是說你必須每個事情都按照單一原則來做 —— 有時候純粹的因為業(yè)務(wù)場景會阻止這種情況的發(fā)生画髓,正如你很快就會看到的那樣录粱。
然而腻格,ViewController沒有理由實現(xiàn)如此多的委托和數(shù)據(jù)源,事實上這樣做會降低視圖控制器的可組合性和可重用性啥繁。如果將這些協(xié)議分割成不同的對象菜职,然后可以在其他ViewController中重用這些對象,或者在同一ViewController中使用不同的對象以在運行時獲得不同的行為旗闽,這是一個巨大的改進(jìn)酬核。
在本文中,我想帶你經(jīng)歷一些 demo适室,這些示例將公共數(shù)據(jù)源和委托從ViewController中取出嫡意,這樣你就可以輕松地應(yīng)用到自己的項目中。
在開始之前捣辆,請使用 Xcode 創(chuàng)建一個新的iOS應(yīng)用程序蔬螟。雖然造成一個非常災(zāi)難性的應(yīng)用程序模版有很多原因,但這導(dǎo)致的結(jié)果是:它會成為你自己所有的日常工作一個搖搖欲墜的基礎(chǔ)汽畴。
我可以寫很多關(guān)于如何修復(fù)它的問題的文章旧巾,但是在這里,我們將做最少的工作來修復(fù)它的兩個問題:ViewController充當(dāng)它的表視圖的數(shù)據(jù)源和委托忍些。
分離數(shù)據(jù)源
Apple 的默認(rèn)模板在 MasterViewController.swift
中有代碼鲁猩,使其充當(dāng)表視圖委托。雖然這對于簡單的應(yīng)用程序或者你正在學(xué)習(xí)的應(yīng)用程序來說是很好的坐昙,但是對于嚴(yán)肅的應(yīng)用程序绳匀,你應(yīng)該(總是)把它分成自己的類,然后根據(jù)需要進(jìn)行復(fù)用炸客。
這里的過程非常簡單疾棵,所以讓我們一步一步地進(jìn)行。
首先痹仙,轉(zhuǎn)到 “File” 菜單并選擇 “New > File”是尔。從 Xcode 提供的列表中選擇 Cocoa Touch Class
,然后按 Next开仰。將其設(shè)為 NSObject
的子類拟枚,給它命名為 “ObjectDataSource
”薪铜,然后單擊 Next 并創(chuàng)建。
下一步是將所有表視圖數(shù)據(jù)源代碼從 MasterViewController.swift
移到 ObjectDataSource.swift
中恩溅。所以隔箍,選擇所有這些代碼并將其剪切到剪貼板:
// MARK: - Table View
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return objects.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let object = objects[indexPath.row] as! NSDate
cell.textLabel!.text = object.description
return cell
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
objects.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
} else if editingStyle == .insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view.
}
}
它們都沒有任何業(yè)務(wù)在 ViewController 中,因此打開 ObjectDataSource.swift
并將其粘貼到該類中脚乡。
在使用 ObjectDataSource
之前蜒滩,我們需要對其進(jìn)行三個小的更改:
- 從所有方法定義中移除
override
。這在 ViewController 中是必需的奶稠,因為我們繼承了UITableViewController
俯艰,但現(xiàn)在我們沒有了。- 通過在
NSObject
旁邊添加UITableViewDataSource
锌订,使類符合UITableViewDataSource
竹握,如下所示:class ObjectDataSource:NSObject,UITableViewDataSource{
辆飘。- 將
var objects=[Any]()
從MasterViewController
上的屬性移動到ObjectDataSource
上的屬性啦辐。
這就完成了 ObjectDataSource
,但是在 MasterViewController
中留下了問題劈猪,因為它試圖引用一個它不再擁有的對象數(shù)組昧甘。
要解決這個問題,我們必須在 MasterViewController
中進(jìn)行兩個更改:使用新的 ObjectDataSource
類給它一個數(shù)據(jù)源屬性战得,然后在使用對象的任何地方引用該數(shù)據(jù)源充边。
首先,打開 MasterViewController.swift
并將此新屬性賦予類:
var dataSource = ObjectDataSource()
其次常侦,把對象的兩個引用更改為 dataSource.objects
浇冰。這意味著將 insertNewObject()
更改為:
dataSource.objects.insert(NSDate(), at: 0)
并將 prepare()
方法更改為:
let object = dataSource.objects[indexPath.row] as! NSDate
是的,我知道聋亡。蘋果的模板代碼很差肘习,但是請記住,我們正在努力做最少的工作來解決我們的兩個問題坡倔。
在這一點上漂佩,代碼編譯得很干凈,但它還不能工作罪塔。為此投蝉,我們需要在MasterViewController
的 viewDidLoad()
方法中進(jìn)行最后一次更改。添加此行:
tableView.dataSource = dataSource
這將告訴表視圖從我們的自定義數(shù)據(jù)源加載其數(shù)據(jù)征堪,現(xiàn)在應(yīng)用程序?qū)⒎祷氐剿鼏訒r的相同狀態(tài)瘩缆。不同之處在于視圖控制器已經(jīng)從 84 行代碼減少到 54 行代碼,而且你現(xiàn)在可以在其他地方使用該數(shù)據(jù)源佃蚜。
這絕對是一個改進(jìn)庸娱,盡管在實踐中着绊,如果你正在使用一個數(shù)據(jù)模型,您可能希望將其移到 coordinator 中熟尉,或者如果在 ViewController 中處理數(shù)據(jù)獲取归露,則可能將其留在 ViewController 中。
分離代理
單一責(zé)任原則 有助于我們將應(yīng)用程序設(shè)計成更小斤儿、更簡單的部分靶擦,然后將這些部分組合在一起,形成更復(fù)雜的組件雇毫。然而,正如我之前所說踩蔚,有時候作為一個務(wù)實的開發(fā)人員會讓你走上一條不同的道路棚放,我想在繼續(xù)之前簡單地討論一下。
您已經(jīng)看到了將表視圖數(shù)據(jù)源輸出到它們自己的對象中是多么簡單馅闽,所以您可能認(rèn)為我們將創(chuàng)建另一個對象作為表視圖委托飘蚯。然而,這一問題更大福也,原因有二:
- 代理/委托 通常需要與數(shù)據(jù)源對話才能執(zhí)行任何操作局骤。例如,當(dāng)一個單元格被點擊時暴凑,需要查看數(shù)據(jù)源以了解這意味著什么峦甩。
-
UITableViewDataSource
和UITableViewDelegate
之間的劃分是奇怪的,似乎是任意的现喳。例如凯傲,數(shù)據(jù)源具有titleForHeaderInSection
,而委托具有viewForHeaderInSection和heightForRowAt
嗦篱。
這意味著將 UITableViewDelegate
拆分為自己的類可能會遇到很多困難冰单。因此,我經(jīng)尘拇伲看到兩種解決方案:
- 將
UITableViewDataSource
和UITableViewDelegate
處理合并到單個類中诫欠。這違背了單一責(zé)任原則,但如果它避免了耦合代碼浴栽,那將是更大的勝利荒叼。 - 將 代理/委托 代碼留在 ViewController 中。只要方法實現(xiàn)沒有包含繁重的邏輯吃度,這并不一定是一個壞的解決方案 —— 只不過是有返回值或著調(diào)用協(xié)調(diào)器而已甩挫。如果你發(fā)現(xiàn)自己加入了業(yè)務(wù)邏輯,你應(yīng)該重新思考椿每。
你喜歡哪個取決于你的個人風(fēng)格伊者,但在我自己的項目中英遭,我更喜歡保持我的 ViewController 盡可能簡單。這意味著它們只是處理視圖生命周期事件(viewDidLoad()
亦渗,等等)挖诸,存儲一些 @IBOutlets
和 @IBActions
,偶爾根據(jù)我正在做的事情來處理模型存儲法精。
記锥嗦伞:這里的目標(biāo)是讓你的應(yīng)用程序設(shè)計更簡單、更容易維護(hù)和更靈活 —— 如果你增加復(fù)雜性只是為了遵循一個原則搂蜓,你最終會遇到問題狼荞。
更簡單的代理/委托
盡管 UITableViewDataSource
和 UITableViewDelegate
很難完全分離,但并不是所有的委托都是這樣的帮碰。相反相味,許多委托很容易分割成不同的類,這樣做殉挽,可以增加相同類型的可重用性丰涉。
讓我們看一個實際的例子:你希望嵌入一個 WKWebView
,它只允許訪問少數(shù)被認(rèn)為對它的安全的子網(wǎng)站斯碌。在一個簡單的實現(xiàn)中一死,你可以將 WKNavigationDelegate
添加到 ViewController 中,給它一個ChildFriendlyStates
數(shù)組作為屬性傻唾,然后編寫一個委托方法投慈,如下所示:
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
if let host = navigationAction.request.url?.host {
if childFriendlySites.contains(where: host.contains) {
decisionHandler(.allow)
return
}
}
decisionHandler(.cancel)
}
重申一下,當(dāng)你構(gòu)建一個小型應(yīng)用程序時冠骄,這種方法是非常好的逛裤,因為要么你只是在學(xué)習(xí),需要動力猴抹,要么你正在構(gòu)建一個原型带族,只是想看看什么是有效的。
但是蟀给,對于任何較大的應(yīng)用程序蝙砌,特別是那些受大規(guī)模ViewController困擾的應(yīng)用程序,你應(yīng)該將這類代碼分成自己的類型:
- 創(chuàng)建一個名為
ChildFriendlyWebDelegate
的類跋理。這需要從NSObject
繼承择克,以便它可以使用WebKit
,并符合WKNavigationDelegate
前普。 - 在文件中
import WebKit
肚邢。 - 將
childfriendlysis
屬性和導(dǎo)航委托代碼放在其中。 - 在 ViewController 創(chuàng)建
ChildFriendlyWebDelegate
的實例,并使其成為web視圖的導(dǎo)航委托骡湖。
這里有一個簡單的實現(xiàn):
import Foundation
import WebKit
class ChildFriendlyWebDelegate: NSObject, WKNavigationDelegate {
var childFriendlySites = ["apple.com", "google.com"]
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
if let host = navigationAction.request.url?.host {
if childFriendlySites.contains(where: host.contains) {
decisionHandler(.allow)
return
}
}
decisionHandler(.cancel)
}
}
這解決了同樣的問題贱纠,同時巧妙地從ViewController中分割出一個離散塊。但你可以而且應(yīng)該更進(jìn)一步响蕴,比如:
func isAllowed(url: URL?) -> Bool {
guard let host = url?.host else { return false }
if childFriendlySites.contains(where: host.contains) {
return true
}
return false
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
if isAllowed(url: navigationAction.request.url) {
decisionHandler(.allow)
} else {
decisionHandler(.cancel)
}
}
這將你的業(yè)務(wù)邏輯分開(“這個網(wǎng)站允許嗎谆焊?,這意味著現(xiàn)在可以編寫測試浦夷,而不必嘗試模擬 WKWebView
辖试。我之前說過,但值得一提的是:任何封裝了比在方法中 return
簡單值的控制器代碼 —— 在接觸到用戶界面時都將更難測試劈狐。在這個重構(gòu)的代碼中罐孝,所有的知識都存儲在 isAllowed()
方法中,因此很容易測試肥缔。
這一變化為我們的應(yīng)用程序帶來了另一個更微妙但同樣重要的改進(jìn):如果我們一開始完成的功能是希望孩子的監(jiān)護(hù)人輸入密碼來解鎖完整的網(wǎng)絡(luò)肾档,現(xiàn)在我們可以通過將 webView.navigationDelegate
設(shè)置為 nil
來啟用它,讓它允許所有站點辫继。
最終實現(xiàn)的結(jié)果是一個我們有了更簡單的視圖控制器,更可測試的代碼俗慈,和更靈活的功能 —— 所以為什么你不雕刻這樣的功能呢姑宽?