Swift Talk #05 Connecting View Controllers

我们把app的流从Storyboard中重构代码到单独的协调类中。这样就避免了View Controller紧耦与他们的上下文。

今天我们来谈谈故事版和如何改进故事版的使用。我们有一个app来显示Table View.如果你点击table里面的一项,会进入详情页。也有一个profile按钮,用来弹出一个模态navigation Controller包裹的另一个view Controller。如果你点击了完成,模态navigation Controller会消失。

s01e01-storyboard.png

在故事版中,我们有一些View Controller: navigation Controller; 视频集的列表;详情View Controller,最终navigation Controller包括了模态个人中心视图。在代码里,我们有3个类:ProfileViewController和DetailViewController.EpisodesViewController有一点复杂。是一个简单的table View Controller,但是又有我们想重构的prepareForSegue方法。prepareForSegue方法区分两个不同的segue和配置各自的view Controller。最终未解开的IBAction拥有segue无论何时模态View Controller消失。

故事版给我们可视化的展示了view Controllers是如何链接的。然而想要改变view Controller的链接就不灵活,应为并不是所有的东西都在故事版中配置,比如segues在故事版中,而prepareForSegue方法又是在View Controller中。我们需要当心代码和故事版的配合。比起把view Controller的链接放在两者都有,我们当然是只放在一个地方会好些。

重构故事版

首先,我们把故事版里面的segues删掉。为了布局这些视图,我们先保留故事版。我们删掉push segue,模态展示segue,并解开IBAction。现在我们的故事版仅仅被用来定义view Controllers。

在我们的代码中,我们先删除prepareForSegue和解开IBAction。现在我们必须找个不同的方式来链接我们的view Controllers。首先我们重写tableView:didSelectRowAtIndexPath这个方法。在这个方法中,我们不想push到下一个View Controller,因为这样就把两个view Controller缠住了。相反我们想从外面控制流。这样的话view Controllers是独立的,而且并不知道他们的被使用的上下文。为了实现这个,当一个row被选中的时候,我们就调用一个方法didSelect传入被选中的episode。

override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    let episode = episodes[indexPath.row]
    didSelect(episode)
}

didSelect的属性很简单,是一个方法类型Episode -> (),并且我们提供一个空的默认实现。

var didSelect: (Episode) -> () = { _ in }

现在我们在AppDelegate中配置这个属性。首先,我们通过window对象获得navigation Controller的引用。其次我们再navigation controller栈中的第一个view Controller获取EpisodesViewController的引用。

let nc = window?.rootViewController as! UINavigationController
let episodesVC = nc.viewControllers[0] as! EpisodesViewController

强制转换并不好,但是我们坚持用故事版就没办法。我可以可以去除强制转换如果我们用纯代码的方式来构建,但是我们使用故事版就必须使用强制转换。然而我们可以创建一个中心地来做这个,举个栗子,在UIStoryboard的扩展中。

Connecting Two View Controllers

既然我们引用了EpisodesViewController,我们能设置didSet属性。匿名方法中传入episode并且在navigation Controller中调用pushViewController。因我们想在故事版中实例化我们的详情View Controller,我们先创建故事版的引用。我们然后可以通过调用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)
}

我们已经连接了这两个view Controllers, 并且我们的episodes View Controller并不知道详情View Controller,因为流仅仅在didSelect的回调用被控制了。最终我们需要配置详情View Controller,所以我们传入我们在didSelect回调中的到的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
    }
}

我们也需要稍后释放个人中心页面,所以我们也需要为它创建一个action。

class ProfileViewController: UIViewController {
    // ...
    @IBAction func close(sender: AnyObject) {
        // TODO
    }
}

在showProfile action中,我们不想写死ProfileViewController的展示。就像刚刚一样,我们在EpisodesViewController中创建一个方法属性的didTapProfile。

class EpisodesViewController: UITableViewController {
    // ...
    var didTapProfile: () -> () = {}

    @IBAction func showProfile(sender: AnyObject) {
        didTapProfile()
    }
}

我们给ProfileViewController中的关闭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中也不好。我们需要继续重构。

创建一个App类

最简单的改进现状的方式就是把所有的代码放到我们自己的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创建一个属性,并实例化这个属性。

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是一堆代码。里面有很多回调,包括了回调中的回调。我们应该通过把这些回调拿出来来改进我们的代码,因为回调的情况只会把这些变得复杂:navigation栈中的每一层会引起另一个嵌套的回调。

删除嵌套回调

放弃用闭包配置回调,我们可以把代码放到didSelectEpisode方法中。

func didSelectEpisode(episode: Episode) {
    let detailVC = storyboard.instantiateViewControllerWithIdentifier("Detail") as! DetailViewController
    detailVC.episode = episode
    navigationController.pushViewController(detailVC, animated: true)
}

在我们的闭包中,我们可以调用didSelectEpisode方法。

episodesVC.didSelect = { episode in
    self.didSelectEpisode(episode)
}

didSelectEpisode听起来像是一个回调方法。改进这个命名,用其他的比如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
        // ...
    }
    // ...
}

为了不写闭包和调用这个方法,我们可以配置didSelect方法,这样写清爽多了。

episodesVC.didSelect = showEpisode

我们可以给个人中心的选择做同样的事情。我们创建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)
}

有了这些方法命名确实增加了可读性,因为这些命名方法比所有这些嵌套的回调都容易理解。

我们成功的让view Controllers简单。仅仅app类知道他们是怎么连接的。为了展示这个改变有多简单,我们可以在一个视频集被点击的时候,展示个人中心view controller。我们只需要在一个地方改变,就能让我们的app不同。

episodesVC.didSelect = { _ in self.showProfile() }

App这个类代码密度比较大,但是其他的代码都很简单而且解耦。还有个问题,当你看到一个持有self引用的闭包的时候,你需要知道是否形成了循环引用。在我们的实例中,没有。举个栗子,只有navigation Controller引用了profile View Controller。一旦你点击了返回,引用页释放了。不过所有代码这些闭包可能会引起你的思考,是不是需要使用weak?当重构的时候,还是容易一不小心就造成循环引用了。

我们的方法可以方便的创建更多可服用的view Controller.举个栗子,我们可以使用一些泛型View Controller在不同的地方复用。让我们期待后面的几个视频集吧。

你可能感兴趣的:(Swift Talk #05 Connecting View Controllers)