我們使用協(xié)議,ViewController容器和泛型等方法來從ViewController中分離出異步加載代碼。
我們繼續(xù)聊聊從網(wǎng)絡(luò)加載數(shù)據(jù)。我們已經(jīng)寫了networking部分,所以今天我們將談?wù)勗赨I中處理異步請求的方法。當我們從網(wǎng)絡(luò)加載數(shù)據(jù),我們總是面對這些問題:我們還沒拿到數(shù)據(jù),但是我們想要給用戶顯示活動或者進程指示器,就是菊花或者進度條。一旦數(shù)據(jù)到了,我們再配置這個視圖。
我們從在一個獨立ViewController應(yīng)用這個模式開始。后面我們找找如何分離加載邏輯的不同的方法。
Making View Controllers Asynchronous
首先我們建立一個簡單的ViewController,EpisodeDetailViewController來簡單顯示一集的題目。在ViewDidLoad中,我們設(shè)定背景顏色和增加一個label。
final class EpisodeDetailViewController: UIViewController {
let titleLabel = UILabel()
convenience init(episode: Episode) {
self.init()
titleLabel.text = episode.title
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .whiteColor()
titleLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(titleLabel)
titleLabel.constrainEdges(toMarginOf: view)
}
}
對于加載的部分,我們給ViewController增加另外的構(gòu)造器。這個構(gòu)造器使用Rsource<Episode>替代Episode。在S01E01章節(jié)中我們討論了關(guān)于網(wǎng)絡(luò)部分和如何處理source。在這個構(gòu)造器中,我們使用sharedWebservice來從數(shù)據(jù)源中加載傳入的數(shù)據(jù)。一旦網(wǎng)絡(luò)層請求完成了,我們通過回調(diào)得到結(jié)果。如果我們不能從結(jié)果中得到dpisode我們僅僅立即返回,并且我們使用guard語句用于早點退出。在guard后面,我們知道我們有了Episode對象并更新label。因我們引用了self,需要使用weak來避免循環(huán)引用。最終當數(shù)據(jù)返回的時候并沒有顯示在屏幕上。最終,我們需要調(diào)用一下父類的init方法。
convenience init(resource: Resource<Episode>) {
self.init()
sharedWebservice.load(resource) { [weak self] result in
guard let value = result.value else { return } // TODO loading error
self?.titleLabel.text = value.title
}
}
使用構(gòu)造器,我們可以更新調(diào)用方法。而不是處理一個episode我們有Resource提供給ViewController.
我們?nèi)匀粵]有一個活動指示器。我們增加一個spinner屬性用來給ViewController存儲UIActivityIndicatorView.我們在發(fā)網(wǎng)絡(luò)請求前發(fā)起spinner。一旦網(wǎng)絡(luò)其你去完成了,停止spinner,無論網(wǎng)絡(luò)請求是否成功。另外,我們用self?引用spinner,來避免循環(huán)引用。
convenience init(resource: Resource<Episode>) {
self.init()
spinner.startAnimating()
sharedWebservice.load(resource) { [weak self] result in
self?.spinner.stopAnimating()
guard let value = result.value else { return } // TODO loading error
self?.titleLabel.text = value.title
}
}
我們在ViewDidLoad中配置spinner。我們把hidesWhenStopped設(shè)置為true,關(guān)閉resizing mask轉(zhuǎn)換,并且藏家一個spinner。最終,我們把他放到中間,使用一個我們的auto layout extension。
spinner.hidesWhenStopped = true
spinner.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(spinner)
spinner.center(inView: view)
顯示spinner,我們?nèi)匀恍枰靡粋€特定的style來初始化,目前我們先簡單用.Gray來初始化。
let spinner = UIActivityIndicatorView(activityIndicatorStyle: .Gray)
有很多樣板代碼來創(chuàng)建和配置activity indicator,并且我們必須在很多不同的ViewController里面重復(fù)。既然如此,當然是把這段邏輯分離出ViewController中比較好。這樣做的話會節(jié)約很多工作,也使得ViewController更簡單。
Creating the Loading Protocol
首先我們嘗試的方法時創(chuàng)建一個協(xié)議,并且把這些代碼拉入到協(xié)議的擴展中。這個方法和蘋果公司在WWDC2015session中面向協(xié)議編程給出了demo。我們開始增加一個Loading協(xié)議,定義spinner為一個只讀屬性。我們也給protocol增加一個load方法,執(zhí)行實際的網(wǎng)絡(luò)請求和開始于停止spinner。這個方法把resource當做參數(shù)。因為我們不能更加明確Resource的泛型。我們給協(xié)議增加一個叫做ResourceType的關(guān)聯(lián)類型,這樣可以處理任何類型。
protocol Loading {
associatedtype ResourceType
var spinner: UIActivityIndicatorView { get }
func load(resource: Resource<ResourceType>)
}
因為我們想提供load方法,我們創(chuàng)建一個協(xié)議擴展。實際上我們在協(xié)議擴展中申明load方法就夠了,因為我們不想讓這個方法被遵循Loading協(xié)議的類復(fù)寫。
protocol Loading {
associatedtype ResourceType
var spinner: UIActivityIndicatorView { get }
}
extension Loading {
func load(resource: Resource<ResourceType>) {
// TODO
}
}
現(xiàn)在我們就移動寫在ViewController里面的代碼到協(xié)議中的load方法中。這個方法仍然執(zhí)行一樣的任務(wù):開始轉(zhuǎn)菊花,從web service中加載數(shù)據(jù),和停止轉(zhuǎn)菊花。為了使這些代碼運作,我們約束這些協(xié)議給UIViewController的實例。
extension Loading where Self: UIViewController {
func load(resource: Resource<ResourceType>) {
spinner.startAnimating()
sharedWebservice.load(resource) { [weak self] result in
self?.spinner.stopAnimating()
guard let value = result.value else { return } // TODO loading error
// TODO configure views
}
}
}
在我們的協(xié)議擴展中,我們不知道在我們從網(wǎng)絡(luò)返回后怎么配置view。因此我們需要代理這個任務(wù)回給ViewController本身。為此我們給協(xié)議增加一個configure方法需要一個ResourceType類型的值。一旦網(wǎng)絡(luò)數(shù)據(jù)返回我們調(diào)用configure方法。
protocol Loading {
// ...
func configure(value: ResourceType)
}
extension Loading where Self: UIViewController {
func load(resource: Resource<ResourceType>) {
spinner.startAnimating()
sharedWebservice.load(resource) { [weak self] result in
self?.spinner.stopAnimating()
guard let value = result.value else { return } // TODO loading error
self?.configure(value)
}
}
}
現(xiàn)在我們可以使ViewController遵循Loading。spinner屬性已經(jīng)存在,所以我們就必須實現(xiàn)configure方法,把label的text設(shè)置成episode的標題。
final class EpisodeDetailViewController: UIViewController, Loading {
// ...
func configure(value: Episode) {
titleLabel.text = value.title
}
// ...
}
我們可以在構(gòu)造器中調(diào)用load方法
final class EpisodeDetailViewController: UIViewController, Loading {
convenience init(resource: Resource<Episode>) {
self.init()
load(resource)
}
// ...
}
我們可以分離這些代碼并且在viewDidLoad中建立spinner,但是我們將先將就一下。
這個已經(jīng)改進了很多,因為我們從ViewController中移除了大量代碼。然而這并不是完美的解決方案。一個缺點是我們僅僅隱藏了之前在ViewController中的代碼到協(xié)議中。ViewController仍和這些代碼緊密耦合。舉個栗子,如果沒有網(wǎng)絡(luò)棧,我們無法實例化EpisodeDetailViewController。這讓測試變得沒必要的復(fù)雜。
Using Container View Controllers
我們試試一個不同的方法,使用container View Controller來分離顯示loading activity和顯示數(shù)據(jù)的部分。Container View Controller會發(fā)起網(wǎng)絡(luò)請求,并且一旦數(shù)據(jù)返回,我們可以增加final View Controller作為子Controller。
第一步是創(chuàng)建一個叫做LoadingViewController的UIViewController.構(gòu)造器使用任何的resource,所以我們必須給構(gòu)造器增加一個泛型參數(shù)。第二參數(shù)是build方法,用于傳入網(wǎng)絡(luò)請求的返回結(jié)果并返回一個View Controller。
final class LoadingViewController: UIViewController {
init<A>(resource: Resource<A>, build: (A) -> UIViewController) {
// TODO
}
}
add方法征程的增加一個child View Controller。首先我們調(diào)用addChildViewController并且增加他的view為subview。然后我們添加一些約束。最終我們調(diào)用Child View Controller的didMoveToParentViewController。
func add(content content: UIViewController) {
addChildViewController(content)
view.addSubview(content.view)
content.view.translatesAutoresizingMaskIntoConstraints = false
content.view.constrainEdges(toMarginOf: view)
content.didMoveToParentViewController(self)
}
為了使LoadingViewController編譯,我們必須增加相應(yīng)的必要構(gòu)造器。Xcode可以使用"fix all in scope"快捷鍵修復(fù)這些問題。
增加spinner到view層級上,我們從EpisodeDetailViewController中的viewDidLoad方法中拷貝代碼。
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .whiteColor()
spinner.hidesWhenStopped = true
spinner.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(spinner)
spinner.center(inView: view)
}
在LoadingViewController中,我們現(xiàn)在可以清理EpisodeDetailViewcontroller并且移除所有沒有用到的代碼。實際上我們僅僅把他轉(zhuǎn)換成原始的狀態(tài)。
為了試試新的LoadingViewController,我們用episode resource來完成實例化。然后我們從編譯方法中返回EpisodeDetailViewController的一個實例。
let episodesVC = LoadingViewController(resource: episodeResource, build: { episode in
return EpisodeDetailViewController(episode: episode)
})
另一個提升是直接使用EpisodeDetailViewController.init作為編譯方法。通過直接調(diào)用構(gòu)造器,我們不需要匿名方法。
let episodesVC = LoadingViewController(resource: episodeResource, build: EpisodeDetailViewController.init)
我們喜歡這個方式是因為EpisodeDetailViewController現(xiàn)在又變得很簡單。也是同步的,因此在分離測試中也很簡單。
然而我們?nèi)匀桓倪MLoadingViewController通過移除其對shared web service的依賴。不傳遞一個resource,我們可以傳進一個實際執(zhí)行加載數(shù)據(jù)的load方法。一旦這個回調(diào)被調(diào)用了,我們可以像之前一樣進行和build方法。load方法參數(shù)是一個單參數(shù)方法,沒有返回類型。這個參數(shù)是一個需要A類型的Result方法。不再調(diào)用sharedWebservice.load,現(xiàn)在我們可以調(diào)用load方法,其他的保持不變。
init<A>(load: ((Result<A>) -> ()) -> (), build: (A) -> UIViewController) {
super.init(nibName: nil, bundle: nil)
spinner.startAnimating()
load() { [weak self] result in
self?.spinner.stopAnimating()
guard let value = result.value else { return } // TODO loading error
let viewController = build(value)
self?.add(content: viewController)
}
}
現(xiàn)在我們移動call到shared web service中去,在我們實例化LoadingViewController的地方。實現(xiàn)load方法很直接,我們傳入callback然后用episode resource調(diào)用shared web service作為完成的回調(diào)。
let sharedWebservice = Webservice()
let episodesVC = LoadingViewController(load: { callback in
sharedWebservice.load(episodeResource, completion: callback)
}, build: EpisodeDetailViewController.init)
如果我們想要實例化LOadingViewController用Resource來加載數(shù)據(jù),為了避免代碼重復(fù),我們可以增加一個便利構(gòu)造器在LOadingViewcontroller的擴展中。然而,loading View
Controller不再依賴于Web Service這樣好多了。loading View Controller不再有任何依賴,并且EpisodeDetailViewController被大幅刪減,使得測試非常容易。
Pros and Cons
使用協(xié)議和使用container View Controller兩者都各自有優(yōu)缺點。使用container View Controller問題在于子View Controller有的時候不能和UIKit很好的配合。舉個列子,如果你把LoadingViewController放進navigation棧里,這個可能會影響UIKit的布局調(diào)整和擴展邊緣。撇開這些問題,這個方法再開發(fā)中還是很有用的。另外,有時候我們使用這個方法,僅僅作為臨時的方法。在這些事例中,這很好的使View Controlller變得即刻異步。在這些地方,我們使用child View Controller,這個方法也可以在最終的代碼中很好的運行。
child View Controller的另外一個問題是Navigation Items無法使用。因此你必須寫額外的代碼。無論如何,在你不改變View Controllers的代碼情況下LoadingViewController變得很順手。舉個栗子,我們用它包裹AVPlayerViewController.
在這些實例中,child View Controllers并不能很好的運行,你可以分離使用另外的方法分離異步加載數(shù)據(jù)這個通用模式。舉個栗子,我們剛剛在使用協(xié)議之前的栗子。協(xié)議方法也有更加輕量的優(yōu)勢,因為他并在View 層級中添加另外的圖層。
另外一個實例我們在我們的項目中使用了LoadingViewController給table View
Controller作一個暫時的解決方案,在我們實現(xiàn)下拉刷新之前。一旦我們使用了下拉刷新,我們簡單把這個包裹溢出了。這是在你的項目開發(fā)中一個很好的工具。