我们把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。

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

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

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

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

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

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

Connecting Two View Controllers

既然我们引用了EpisodesViewController,我们能设置didSet属性。匿名方法中传入episode并且在navigation Controller中调用pushViewController。因我们想在故事版中实例化我们的详情View Controller,我们先创建故事版的引用。我们然后可以通过调用instantiateViewControllerWithIdentifier来得到详情View Controller.

  1. let storyboard = UIStoryboard(name: "Main", bundle: nil)
  2. episodesVC.didSelect = { episode in
  3. let detailVC = storyboard.instantiateViewControllerWithIdentifier("Detail") as! DetailViewController
  4. nc.pushViewController(detailVC, animated: true)
  5. }

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

  1. episodesVC.didSelect = { episode in
  2. let detailVC = storyboard.instantiateViewControllerWithIdentifier("Detail") as! DetailViewController
  3. detailVC.episode = episode
  4. nc.pushViewController(detailVC, animated: true)
  5. }

在我们的app中,我们可以选择cell并且详情View Controller被正确的配置。然而,个人中心页面还没有弄好。在我们的故事板中,我们需要连接个人中心按钮和episodes view controller中的一个action。

  1. class EpisodesViewController: UITableViewController {
  2. // ...
  3. @IBAction func showProfile(sender: AnyObject) {
  4. // TODO
  5. }
  6. }

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

  1. class ProfileViewController: UIViewController {
  2. // ...
  3. @IBAction func close(sender: AnyObject) {
  4. // TODO
  5. }
  6. }

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

  1. class EpisodesViewController: UITableViewController {
  2. // ...
  3. var didTapProfile: () -> () = {}
  4. @IBAction func showProfile(sender: AnyObject) {
  5. didTapProfile()
  6. }
  7. }

我们给ProfileViewController中的关闭action做同样的事。

  1. class ProfileViewController: UIViewController {
  2. // ...
  3. var didTapClose: () -> () = {}
  4. @IBAction func close(sender: AnyObject) {
  5. didTapClose()
  6. }
  7. }

在AppDelegate中,我们用闭包来配置didTapProfile属性,闭包中我们实例化ProfileViewController并且显示。

  1. episodesVC.didTapProfile = {
  2. let profileVC = storyboard.instantiateViewControllerWithIdentifier("Profile") as! UINavigationController
  3. nc.presentViewController(profileVC, animated: true, completion: nil)
  4. }

为了让消失一样起作用,我们也这样配置didTapClose属性。在这个闭包中我们需要在navigation Controller中弹出ProfileViewController。

  1. episodesVC.didTapProfile = {
  2. let profileNC = storyboard.instantiateViewControllerWithIdentifier("Profile") as! UINavigationController
  3. let profileVC = profileNC.viewControllers[0] as! ProfileViewController
  4. profileVC.didTapClose = {
  5. nc.dismissViewControllerAnimated(true, completion: nil)
  6. }
  7. nc.presentViewController(profileNC, animated: true, completion: nil)
  8. }

这个模式解耦了我们的view Controllers,他们相互不知道,也不相互显示。他们在外面的一个集中的地方被连接起来,因此这些View Controllers并不知道他们被包含在一个navigation Controller中。然而把所有的这些代码放到AppDelegate中也不好。我们需要继续重构。

创建一个App类

最简单的改进现状的方式就是把所有的代码放到我们自己的App类里面。

  1. class App {
  2. init(window: UIWindow) {
  3. let nc = window.rootViewController as! UINavigationController
  4. let episodesVC = nc.viewControllers[0] as! EpisodesViewController
  5. let storyboard = UIStoryboard(name: "Main", bundle: nil)
  6. episodesVC.didSelect = { episode in
  7. let detailVC = storyboard.instantiateViewControllerWithIdentifier("Detail") as! DetailViewController
  8. detailVC.episode = episode
  9. nc.pushViewController(detailVC, animated: true)
  10. }
  11. episodesVC.didTapProfile = {
  12. let profileNC = storyboard.instantiateViewControllerWithIdentifier("Profile") as! UINavigationController
  13. let profileVC = profileNC.viewControllers[0] as! ProfileViewController
  14. profileVC.didTapClose = {
  15. nc.dismissViewControllerAnimated(true, completion: nil)
  16. }
  17. nc.presentViewController(profileNC, animated: true, completion: nil)
  18. }
  19. }
  20. }

在AppDelegate中,我们给App创建一个属性,并实例化这个属性。

  1. class AppDelegate: UIResponder, UIApplicationDelegate {
  2. var window: UIWindow?
  3. var app: App?
  4. func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
  5. if let window = window {
  6. app = App(window: window)
  7. }
  8. return true
  9. }
  10. }

我们的问题在于我们的App.init是一堆代码。里面有很多回调,包括了回调中的回调。我们应该通过把这些回调拿出来来改进我们的代码,因为回调的情况只会把这些变得复杂:navigation栈中的每一层会引起另一个嵌套的回调。

删除嵌套回调

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

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

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

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

didSelectEpisode听起来像是一个回调方法。改进这个命名,用其他的比如showEpisode可能会更好。

  1. func showEpisode(episode: Episode) {
  2. let detailVC = storyboard.instantiateViewControllerWithIdentifier("Detail") as! DetailViewController
  3. detailVC.episode = episode
  4. navigationController.pushViewController(detailVC, animated: true)
  5. }

为了编译这些,我们把故事版和navigation Controller拉出来放到App类中。

  1. final class App {
  2. let storyboard = UIStoryboard(name: "Main", bundle: nil)
  3. let navigationController: UINavigationController
  4. init(window: UIWindow) {
  5. navigationController = window.rootViewController as! UINavigationController
  6. // ...
  7. }
  8. // ...
  9. }

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

  1. episodesVC.didSelect = showEpisode

我们可以给个人中心的选择做同样的事情。我们创建showProfile方法并从闭包中拉出来。

  1. init(window: UIWindow) {
  2. navigationController = window.rootViewController as! UINavigationController
  3. let episodesVC = navigationController.viewControllers[0] as! EpisodesViewController
  4. episodesVC.didSelect = showEpisode
  5. episodesVC.didTapProfile = showProfile
  6. }
  7. func showProfile() {
  8. let profileNC = self.storyboard.instantiateViewControllerWithIdentifier("Profile") as! UINavigationController
  9. let profileVC = profileNC.viewControllers[0] as! ProfileViewController
  10. profileVC.didTapClose = {
  11. self.navigationController.dismissViewControllerAnimated(true, completion: nil)
  12. }
  13. navigationController.presentViewController(profileNC, animated: true, completion: nil)
  14. }

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

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

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

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

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