This episode is freely available thanks to the support of our subscribers

Subscribers get exclusive access to new and all previous subscriber-only episodes, video downloads, and 30% discount for team members. Become a Subscriber

We refactor our code by moving the app's flow from the storyboard into a separate coordinator class. This avoids view controllers having implicit knowledge of their context.

00:06 Today we'll talk about storyboards and how we can refactor some parts of them. 00:13 We have an app that displays a table view. If you tap on one of the items in the table, it shows the detail view controller. There's also a profile button, which pops up a modal navigation controller with another view controller inside. If you click "Done," the modal navigation controller gets dismissed:

s01e01-storyboard.png

00:33 In the storyboard, we have a few view controllers: the navigation controller; the episodes list; the detail view controller; and finally, the navigation controller, which contains the modal profile view. 00:51 In the code, we have three classes. The first two are simple: the ProfileViewController and the DetailViewController. The EpisodesViewController is a little bit more complicated. It's a simple table view controller, but it also has the prepareForSegue method, which we want to refactor. The prepareForSegue method distinguishes between two different segues and configures the respective view controllers. 01:34 Finally, there's the unwind IBAction, which gets a segue whenever the modal view controller is dismissed.

01:59 It's nice that the storyboard gives us a visual representation of how the view controllers are connected. 02:11 However, it's difficult to change the way the view controllers are connected, because not everything is done in the storyboard: we have segues in the storyboard, but we have the prepareForSegue methods in the view controllers. We need to be careful to ensure that the code matches up with the storyboard. Instead of having the connections in both the storyboard and the code, it would be nice to have a single place where this all happens.

Refactoring the Storyboard

02:44 First, we remove some segues from the storyboard. We'll keep the storyboard around for now, in order to define our view controllers and lay out views. 03:03 We remove the push segue, the modal presentation segue, and the unwind IBAction. 03:25 Now our storyboard is only used to define the view controllers.

03:33 In our code, we start by removing the prepareForSegue method, along with the unwind IBAction. 03:50 Now we have to find a different way to connect our view controllers. 03:58 The starting point is to override the tableView:didSelectRowAtIndexPath method. 04:07 Within this method, we don't want to push the next view controller, because it would entangle these two view controllers. Instead, we want to control this flow from the outside. That way, the view controllers are standalone and don't know about the context they're used in. 04:28 To achieve that, when a row gets selected, we just call a function, didSelect, with the selected episode:

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

04:51 The didSelect property is simple — it's a function with type Episode -> (), and we provide a default implementation, which does nothing:

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

05:17 Now we configure this property in our AppDelegate. 05:21 First, we get a reference to the navigation controller via the window object. 05:45 Second, we get a reference to the EpisodesViewController, which is the first view controller in the navigation controller's stack:

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

06:03 The force casting isn't nice, but it's a result of us still holding on to the storyboard. We could get rid of it if we would instantiate all our view controllers in code, but as long as we use storyboards, we have to deal with force casting. However, we can create a central place where we do this — for example, in an extension on UIStoryboard.

Connecting Two View Controllers

06:42 Now that we have a reference to the EpisodesViewController, we can set the didSelect property. The anonymous function gets the episode in and calls pushViewController on the navigation controller. 07:08 Since we want to instantiate our detail view controller from the storyboard, we first create a reference to the storyboard. We can then get our detail view controller by calling instantiateViewControllerWithIdentifier:

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

07:49 We've connected these two view controllers, and our episodes view controller doesn't know anything about the detail view controller, as the flow is controlled only within the didSelect callback. 08:01 Finally, we need to configure the detail view controller, so we pass in the episode we received in the didSelect callback:

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

08:24 In our app, we can now select cells, and the detail view controller gets configured correctly. However, the profile screen doesn't work yet. 08:35 In our storyboard, we have to hook up the "My Profile" button with an action in the episodes view controller:

class EpisodesViewController: UITableViewController {
    // ...
    @IBAction func showProfile(sender: AnyObject) {
        // TODO
    }
}

09:04 We also need to dismiss the profile screen at a later point, so we'll create an action for that as well:

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

09:31 In the showProfile action, we don't want to hardcode the presentation of the ProfileViewController. Just like before, we introduce a function, didTapProfile, which we hand into EpisodesViewController:

class EpisodesViewController: UITableViewController {
    // ...
    var didTapProfile: () -> () = {}
    
    @IBAction func showProfile(sender: AnyObject) {
        didTapProfile()
    }
}

10:13 We do the same thing for our close action in ProfileViewController:

class ProfileViewController: UIViewController {
    // ...
    var didTapClose: () -> () = {}

    @IBAction func close(sender: AnyObject) {
        didTapClose()
    }
}

10:45 In the AppDelegate, we configure the didTapProfile property with a closure, which instantiates the ProfileViewController and presents it:

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

11:37 To make the dismissal work as well, we have to set the didTapClose property. For this, we need to get the ProfileViewController out of the navigation controller:

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)
}

12:44 This pattern decouples our view controllers: they don't know about each other and they don't present each other. They're connected in a central place outside, so the view controllers don't even know that they're inside a navigation controller. 13:02 However, it's not nice that all this code is still living in the AppDelegate. We need to refactor that.

Creating an App class

13:12 The easiest way to improve this situation is to put this code inside its own class, 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)
        }
    }
}

13:54 In the AppDelegate, we create a property for App and instantiate it:

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
    }
}

14:27 One problem is that our App.init is a large blob of code. There are many callbacks, including callbacks inside callbacks. We should improve the code by factoring those callbacks out, since the callback situation will only get more complicated with time: each level in the navigation stack will result in another nested callback.

Reducing Nested Callbacks

14:50 Instead of configuring a callback with a closure, we can put that code into a method called didSelectEpisode:

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

15:19 In our closure, we can call the didSelectEpisode method:

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

15:27 didSelectEpisode sounds like a delegate method. To improve the naming, something more active — like showEpisode — would be better:

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

15:47 To make it compile, we pull the storyboard and the navigation controller out into properties on App:

final class App {
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let navigationController: UINavigationController

    init(window: UIWindow) {
        navigationController = window.rootViewController as! UINavigationController
        // ...
    }
    // ...
}

16:44 Instead of writing the closure and calling the method, we can directly assign the method to didSelect, which cleans up the code a lot:

episodesVC.didSelect = showEpisode

17:00 We can do the same thing for the profile selection. We create a method called showProfile and pull out the closure:

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)
}

17:33 Having these method names really helps readability, as the named methods are easier to understand than all those nested callbacks.

17:47 We managed to keep the view controllers simple; only the app class knows how they're connected. 18:04 To show how easy it is to change things around, we can show the profile view controller when an episode is selected. 18:15 We only need to make changes in this one place, and now our app behaves differently:

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

18:28 The App class is a dense piece of code, but the rest of the code is very simple and decoupled. 18:52 One problem though: when you see a closure with a self reference inside, you start wondering if there are reference cycles. In our case, there aren't. For example, only the navigation controller references the profile view controller. Once you hit dismiss, the reference goes away. Still, having all these closures in your code might make you think: "Is this still good, or do I have to mark something as weak?" 19:27 When refactoring, it's also easy to introduce reference cycles accidentally.

19:51 Our approach lends itself well to creating more reusable view controllers. For example, we can introduce some generic view controllers to reuse in different places. Let's look at that in a future episode.

Resources

  • Playground

    Written in Swift 2.2

  • Episode Video

    Become a subscriber to download episode videos.

Related Blogposts

In Collection

20 Episodes · 7h25min

See All Collections

Episode Details

Recent Episodes

See All

Unlock Full Access

Subscribe to Swift Talk

  • Watch All Episodes

    A new episode every week

  • icon-benefit-download Created with Sketch.

    Download Episodes

    Take Swift Talk with you when you're offline

  • Support Us

    With your help we can keep producing new episodes