Safe segues in Storyboards

Safe segues in Storyboards

This article is ❄️❄️❄️ … or not! I will talk about our Qold® mobile app and how it was built using Storyboards. I hope that you know already what Qold is but if not then take a look at how it started.

Figure 1: Qold iOS app

We decided to start with a basic functionality for the app. A light release with something fancy, clear and effective. We wanted to avoid pushing everything from a web dashboard into our mobile app. So, what are the main features that it should have? First, we want to keep track of all user Qold devices. Second we want to change each Qold property for example high and low temperature limits, mobile phone number for direct notifications or periodicity reports. With these features in mind and after design, we knew it would be a simple app to build and that it would require only one developer to do the job.

Storyboards

Storyboards were already discussed in a previous article. But, despite some of the cons, using storyboards still made sense in this project. They allowed us to put together a simple application in just a few days.

Until now, we never implemented any application fully written in Swift. So, it would be great to take this change to start doing it. However, when I began implementing, I noticed that Storyboards with Swift were quite annoying. There’s a conflict between Swift and Storyboards because Swift is focused on safety but the current API is stringly typed!

Within Storyboards every UIViewController represents one screen of content. These scenes are linked together with Segues and those define the transitions between one scene to another. The transition between scenes is done automatically by the system, the target view controller for a segue is created by UIKit and it works by calling the init?(coder:) on the UIViewController. This means that we can’t use dependency injection via initializer. Even when we don’t use the constructor injection, dependencies still have to be provided in one way or another and that’s why segues are important.

If you want to initiate a segue, you need to specify an identifier that must be a defined string from the current view controller’s storyboard file. Then you need to prepare your segue, to configure the new view controller prior to it being displayed, comparing the identifier string to setup correctly everything we need.

E.g., we will end up with code like this:

self.performSegueWithIdentifier("LoginViewController", sender: nil)
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    switch segue.identifier {
    case "LoginViewController"?:
        guard let loginViewController = segue.destinationViewController as? LoginViewController else { return }
 		...
    default:
        break;
    }
}

So, what’s wrong with this? In the beginning I was doing a lot of changes on the storyboard file and that was leading to wrong identifiers. It’s really easy to screw up your app flow when you want to rename an identifier or you want to introduce a new view controller between two screens.

One solution

Let’s introduce some type-safety in place of these strings using Enumerations! I defined an enum for each possible destination:

enum StoryboardDestination {
    case Login
    case DeviceGroups(userAuth: UserAuthentication)
    case Devices(userAuth: UserAuthentication, model: DeviceGroup)
}

Then I created an extension for UIViewController where I relate each enum case to one segue in a way where it’s possible to perform a segue with the StoryboardDestination like performSegueWithIdentifier(.Devices(userAuth: currentAuth, model: deviceGroups[indexPath.row])). The enum case will have all the dependencies needed.

Note: it’s important the switch statement to be exhaustive when considering this solution because, if a new case e.g. .Logout is created, the code will not compile because it does not consider the complete list of StoryboardDestination cases.

extension UIViewController {
    
    func performSegueWithIdentifier(destination: StoryboardDestination) {
        let segue: String
        
        switch destination {
        case .Login:
            segue = "LoginSegue"
        case .DeviceGroups(_):
            segue = "DeviceGroupsSegue"
        case .Devices(_, _):
            segue = "DeviceGroupDetailsSegue"
        }
        
        performSegueWithIdentifier(segue, sender: Box(destination))
    }
    
}

I established the relationship between the destination and the Segue identifier. The sender must be a class so I wrapped the destination in a class. I also needed a protocol to determine which UIViewController can receive dependencies from a segue.

protocol StoryboardDependencies {
    func assignDependencies(dependencies: Box<StoryboardDestination>)
}

Then, I created a class that represents a Storyboard view controller. It overrides the prepareForSegue method, giving the ability to receive all dependencies with safety.

class StoryboardViewController: UIViewController {
    
    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        var anyDestination: AnyObject = segue.destinationViewController
        // Navigation
        if let navigationController = anyDestination as? UINavigationController,
           let topVC = navigationController.topViewController {
            anyDestination = topVC
        }
        // TabBar
        if let tapBarController = anyDestination as? UITabBarController, let viewControllers = tapBarController.viewControllers {
            if viewControllers.count > 0 {
                anyDestination = viewControllers[0]
            }
        }
        // Assign dependencies
        if let destinationViewController = anyDestination as? StoryboardDependencies,
           let dependencies = sender as? Box<StoryboardDestination> {
            destinationViewController.assignDependencies(dependencies)
        }
    }
    
}

Finally, we can use all what I explained before in a view controller. The DevicesViewController will receive automatically all his dependencies through the overridden StoryboardViewController.prepareForSegue method and through the implementation of the StoryboardDependencies protocol.

class DevicesViewController: StoryboardViewController, StoryboardDependencies {

    func assignDependencies(dependencies: Box<StoryboardDestination>) {
        switch dependencies.value {
        case .Devices(let userAuth, let model):
            self.userAuth = userAuth
            deviceGroup = DeviceGroupViewModel(model)
        default:
            break
        }
    }

}

Conclusion

And that’s it. I think it’s a simple way to keep using Storyboards in a safe way and to turn push runtime crashes into compiler crashes giving us more confidence on making changes.

Figure 2: more Qold iOS app

#mobile #development

Ricardo Pereira

More posts

Share This Post

Right Talent, Right Time

Turnkey technical teams and solo specialists
when you need them for greatest impact.

Work with us