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.
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.
The Non-Technical Founders survival guide
How to spot, avoid & recover from 7 start-up scuppering traps.
The Non-Technical Founders survival guide: How to spot, avoid & recover from 7 start-up scuppering traps.