我們把app的流從Storyboard中重構(gòu)代碼到單獨的協(xié)調(diào)類中。這樣就避免了View Controller緊耦與他們的上下文豺旬。
今天我們來談?wù)劰适掳婧腿绾胃倪M故事版的使用钠惩。我們有一個app來顯示Table View.如果你點擊table里面的一項,會進入詳情頁哈垢。也有一個profile按鈕妻柒,用來彈出一個模態(tài)navigation Controller包裹的另一個view Controller扛拨。如果你點擊了完成耘分,模態(tài)navigation Controller會消失。
在故事版中绑警,我們有一些View Controller: navigation Controller; 視頻集的列表求泰;詳情View Controller,最終navigation Controller包括了模態(tài)個人中心視圖计盒。在代碼里渴频,我們有3個類:ProfileViewController和DetailViewController.EpisodesViewController有一點復(fù)雜。是一個簡單的table View Controller北启,但是又有我們想重構(gòu)的prepareForSegue方法卜朗。prepareForSegue方法區(qū)分兩個不同的segue和配置各自的view Controller。最終未解開的IBAction擁有segue無論何時模態(tài)View Controller消失咕村。
故事版給我們可視化的展示了view Controllers是如何鏈接的场钉。然而想要改變view Controller的鏈接就不靈活,應(yīng)為并不是所有的東西都在故事版中配置懈涛,比如segues在故事版中逛万,而prepareForSegue方法又是在View Controller中。我們需要當心代碼和故事版的配合批钠。比起把view Controller的鏈接放在兩者都有宇植,我們當然是只放在一個地方會好些。
重構(gòu)故事版
首先埋心,我們把故事版里面的segues刪掉指郁。為了布局這些視圖,我們先保留故事版拷呆。我們刪掉push segue闲坎,模態(tài)展示segue,并解開IBAction。現(xiàn)在我們的故事版僅僅被用來定義view Controllers箫柳。
在我們的代碼中手形,我們先刪除prepareForSegue和解開IBAction。現(xiàn)在我們必須找個不同的方式來鏈接我們的view Controllers悯恍。首先我們重寫tableView:didSelectRowAtIndexPath這個方法库糠。在這個方法中,我們不想push到下一個View Controller涮毫,因為這樣就把兩個view Controller纏住了瞬欧。相反我們想從外面控制流。這樣的話view Controllers是獨立的罢防,而且并不知道他們的被使用的上下文艘虎。為了實現(xiàn)這個,當一個row被選中的時候咒吐,我們就調(diào)用一個方法didSelect傳入被選中的episode野建。
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let episode = episodes[indexPath.row]
didSelect(episode)
}
didSelect的屬性很簡單,是一個方法類型Episode -> (),并且我們提供一個空的默認實現(xiàn)恬叹。
var didSelect: (Episode) -> () = { _ in }
現(xiàn)在我們在AppDelegate中配置這個屬性候生。首先,我們通過window對象獲得navigation Controller的引用绽昼。其次我們再navigation controller棧中的第一個view Controller獲取EpisodesViewController的引用唯鸭。
let nc = window?.rootViewController as! UINavigationController
let episodesVC = nc.viewControllers[0] as! EpisodesViewController
強制轉(zhuǎn)換并不好,但是我們堅持用故事版就沒辦法硅确。我可以可以去除強制轉(zhuǎn)換如果我們用純代碼的方式來構(gòu)建目溉,但是我們使用故事版就必須使用強制轉(zhuǎn)換。然而我們可以創(chuàng)建一個中心地來做這個菱农,舉個栗子缭付,在UIStoryboard的擴展中。
Connecting Two View Controllers
既然我們引用了EpisodesViewController,我們能設(shè)置didSet屬性大莫。匿名方法中傳入episode并且在navigation Controller中調(diào)用pushViewController蛉腌。因我們想在故事版中實例化我們的詳情View Controller,我們先創(chuàng)建故事版的引用只厘。我們?nèi)缓罂梢酝ㄟ^調(diào)用instantiateViewControllerWithIdentifier來得到詳情View Controller.
let storyboard = UIStoryboard(name: "Main", bundle: nil)
episodesVC.didSelect = { episode in
let detailVC = storyboard.instantiateViewControllerWithIdentifier("Detail") as! DetailViewController
nc.pushViewController(detailVC, animated: true)
}
我們已經(jīng)連接了這兩個view Controllers, 并且我們的episodes View Controller并不知道詳情View Controller烙丛,因為流僅僅在didSelect的回調(diào)用被控制了。最終我們需要配置詳情View Controller羔味,所以我們傳入我們在didSelect回調(diào)中的到的episode河咽。
episodesVC.didSelect = { episode in
let detailVC = storyboard.instantiateViewControllerWithIdentifier("Detail") as! DetailViewController
detailVC.episode = episode
nc.pushViewController(detailVC, animated: true)
}
在我們的app中,我們可以選擇cell并且詳情View Controller被正確的配置赋元。然而忘蟹,個人中心頁面還沒有弄好飒房。在我們的故事板中,我們需要連接個人中心按鈕和episodes view controller中的一個action媚值。
class EpisodesViewController: UITableViewController {
// ...
@IBAction func showProfile(sender: AnyObject) {
// TODO
}
}
我們也需要稍后釋放個人中心頁面狠毯,所以我們也需要為它創(chuàng)建一個action。
class ProfileViewController: UIViewController {
// ...
@IBAction func close(sender: AnyObject) {
// TODO
}
}
在showProfile action中褥芒,我們不想寫死ProfileViewController的展示嚼松。就像剛剛一樣,我們在EpisodesViewController中創(chuàng)建一個方法屬性的didTapProfile锰扶。
class EpisodesViewController: UITableViewController {
// ...
var didTapProfile: () -> () = {}
@IBAction func showProfile(sender: AnyObject) {
didTapProfile()
}
}
我們給ProfileViewController中的關(guān)閉action做同樣的事献酗。
class ProfileViewController: UIViewController {
// ...
var didTapClose: () -> () = {}
@IBAction func close(sender: AnyObject) {
didTapClose()
}
}
在AppDelegate中,我們用閉包來配置didTapProfile屬性坷牛,閉包中我們實例化ProfileViewController并且顯示罕偎。
episodesVC.didTapProfile = {
let profileVC = storyboard.instantiateViewControllerWithIdentifier("Profile") as! UINavigationController
nc.presentViewController(profileVC, animated: true, completion: nil)
}
為了讓消失一樣起作用,我們也這樣配置didTapClose屬性京闰。在這個閉包中我們需要在navigation Controller中彈出ProfileViewController颜及。
episodesVC.didTapProfile = {
let profileNC = storyboard.instantiateViewControllerWithIdentifier("Profile") as! UINavigationController
let profileVC = profileNC.viewControllers[0] as! ProfileViewController
profileVC.didTapClose = {
nc.dismissViewControllerAnimated(true, completion: nil)
}
nc.presentViewController(profileNC, animated: true, completion: nil)
}
這個模式解耦了我們的view Controllers,他們相互不知道忙干,也不相互顯示器予。他們在外面的一個集中的地方被連接起來浪藻,因此這些View Controllers并不知道他們被包含在一個navigation Controller中捐迫。然而把所有的這些代碼放到AppDelegate中也不好。我們需要繼續(xù)重構(gòu)爱葵。
創(chuàng)建一個App類
最簡單的改進現(xiàn)狀的方式就是把所有的代碼放到我們自己的App類里面施戴。
class App {
init(window: UIWindow) {
let nc = window.rootViewController as! UINavigationController
let episodesVC = nc.viewControllers[0] as! EpisodesViewController
let storyboard = UIStoryboard(name: "Main", bundle: nil)
episodesVC.didSelect = { episode in
let detailVC = storyboard.instantiateViewControllerWithIdentifier("Detail") as! DetailViewController
detailVC.episode = episode
nc.pushViewController(detailVC, animated: true)
}
episodesVC.didTapProfile = {
let profileNC = storyboard.instantiateViewControllerWithIdentifier("Profile") as! UINavigationController
let profileVC = profileNC.viewControllers[0] as! ProfileViewController
profileVC.didTapClose = {
nc.dismissViewControllerAnimated(true, completion: nil)
}
nc.presentViewController(profileNC, animated: true, completion: nil)
}
}
}
在AppDelegate中,我們給App創(chuàng)建一個屬性萌丈,并實例化這個屬性赞哗。
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var app: App?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
if let window = window {
app = App(window: window)
}
return true
}
}
我們的問題在于我們的App.init是一堆代碼。里面有很多回調(diào)辆雾,包括了回調(diào)中的回調(diào)肪笋。我們應(yīng)該通過把這些回調(diào)拿出來來改進我們的代碼,因為回調(diào)的情況只會把這些變得復(fù)雜:navigation棧中的每一層會引起另一個嵌套的回調(diào)度迂。
刪除嵌套回調(diào)
放棄用閉包配置回調(diào)藤乙,我們可以把代碼放到didSelectEpisode方法中。
func didSelectEpisode(episode: Episode) {
let detailVC = storyboard.instantiateViewControllerWithIdentifier("Detail") as! DetailViewController
detailVC.episode = episode
navigationController.pushViewController(detailVC, animated: true)
}
在我們的閉包中惭墓,我們可以調(diào)用didSelectEpisode方法坛梁。
episodesVC.didSelect = { episode in
self.didSelectEpisode(episode)
}
didSelectEpisode聽起來像是一個回調(diào)方法。改進這個命名腊凶,用其他的比如showEpisode可能會更好划咐。
func showEpisode(episode: Episode) {
let detailVC = storyboard.instantiateViewControllerWithIdentifier("Detail") as! DetailViewController
detailVC.episode = episode
navigationController.pushViewController(detailVC, animated: true)
}
為了編譯這些拴念,我們把故事版和navigation Controller拉出來放到App類中。
final class App {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let navigationController: UINavigationController
init(window: UIWindow) {
navigationController = window.rootViewController as! UINavigationController
// ...
}
// ...
}
為了不寫閉包和調(diào)用這個方法褐缠,我們可以配置didSelect方法政鼠,這樣寫清爽多了。
episodesVC.didSelect = showEpisode
我們可以給個人中心的選擇做同樣的事情队魏。我們創(chuàng)建showProfile方法并從閉包中拉出來缔俄。
init(window: UIWindow) {
navigationController = window.rootViewController as! UINavigationController
let episodesVC = navigationController.viewControllers[0] as! EpisodesViewController
episodesVC.didSelect = showEpisode
episodesVC.didTapProfile = showProfile
}
func showProfile() {
let profileNC = self.storyboard.instantiateViewControllerWithIdentifier("Profile") as! UINavigationController
let profileVC = profileNC.viewControllers[0] as! ProfileViewController
profileVC.didTapClose = {
self.navigationController.dismissViewControllerAnimated(true, completion: nil)
}
navigationController.presentViewController(profileNC, animated: true, completion: nil)
}
有了這些方法命名確實增加了可讀性,因為這些命名方法比所有這些嵌套的回調(diào)都容易理解器躏。
我們成功的讓view Controllers簡單俐载。僅僅app類知道他們是怎么連接的。為了展示這個改變有多簡單登失,我們可以在一個視頻集被點擊的時候遏佣,展示個人中心view controller。我們只需要在一個地方改變揽浙,就能讓我們的app不同状婶。
episodesVC.didSelect = { _ in self.showProfile() }
App這個類代碼密度比較大,但是其他的代碼都很簡單而且解耦馅巷。還有個問題膛虫,當你看到一個持有self引用的閉包的時候,你需要知道是否形成了循環(huán)引用钓猬。在我們的實例中稍刀,沒有。舉個栗子敞曹,只有navigation Controller引用了profile View Controller账月。一旦你點擊了返回,引用頁釋放了澳迫。不過所有代碼這些閉包可能會引起你的思考局齿,是不是需要使用weak?當重構(gòu)的時候橄登,還是容易一不小心就造成循環(huán)引用了抓歼。
我們的方法可以方便的創(chuàng)建更多可服用的view Controller.舉個栗子,我們可以使用一些泛型View Controller在不同的地方復(fù)用拢锹。讓我們期待后面的幾個視頻集吧。