Image credit: Apple

Xcode: A Better Way to Deal with Storyboards

Tips and tricks to enjoy the Interface Builder

Apple have made the great improvements in the Interface Builder in Xcode 8. Using the size classes became more intuitive, ability to zoom your storyboard is very convenient, and the full preview right in the Interface Builder is just amazing. For those who were hesitant about using Interface Builder, this may become a deal breaker.

On the other hand, many developers still have some troubles with the Interface Builder when they build big multi-screen apps with complex navigation.

In this article, I will share some of the good practices when you deal with storyboards and nibs in your project. Either you was using Interface Builder before, or you just making the first step in this direction, these tips may be useful for you.


1. If you work in team, use separate storyboard for every screen. If you work alone, it’s still a good habit.

Do you have a single main.storyboard file in your project that looks similar to this?

This looks good from the designer stand-point: you can easily see the complete UI and navigation flow. That’s exactly what the Interface Builder was created for.

But for the developer this may cause multiple problems.

  • Source control: Storyboard merging conflicts are very difficult to solve, so simply working in separate storyboards will make your team life easier.
  • Storyboard file become heavy and hard to navigate in. How many times have you unintentionally changed the constraint by a single click in the wrong ViewController?
  • You need to assign the storyboard ID for every ViewController, that is error-prone: you need to hard-code this ID every time you want to use the ViewController in code

How to connect different storyboards in your project? There are two ways.

1. Use storyboard referencing introduced in Xcode 7

2. Connect storyboards in code.

You can read more about the first way here.

I will cover the second way, as it’s still common used for complex projects.

2. Use the same name for storyboard file and associated viewController subclass.

This will simplify the naming convention, and also give you some benefits in Advice #3.

3. Initialize storyboard right inside its UIViewController subclass.

When it comes to initializing a storyboard-based viewController in code, I often see the following code:

let storyboard = UIStoryboard(name: “Main”, bundle: nil)
let homeViewController = storyboard.instantiateViewController(withIdentifier: “HomeViewController”)

This doesn’t look clear: you need to name the storyboard, you need to provide the viewController storyboard ID, and you need to use this pattern every time, when you create your HomeViewController.

A better way will be to move this code inside the viewController subclass and use a static method to initialize it with storyboard:

class HomeViewController: UIViewController { 
      static func storyboardInstance() -> HomeViewController? { 
let storyboard = UIStoryboard(name:
“HomeViewController”, bundle: nil) return
storyboard.instantiateInitialViewController() as?
HomeViewController
}
}

If you follow the previous advice, you can avoid hard-typing the storyboard name and use the className:

let storyboard = UIStoryboard(name: String.className(self), bundle: nil)
Make sure your storyboard file has the same name as the actual class. Otherwise, the app will crash when you try to create a reference to this storyboard.

That makes your code even more readable and less error-prone:

class HomeViewController: UIViewController {
     static func storyboardInstance() -> HomeViewController? { 
let storyboard = UIStoryboard(name: String.className(self),
bundle: nil) return
storyboard.instantiateInitialViewController() as?
HomeViewController
}
}
If you want to access the ViewController by instantiateInitialViewController(), make sure you mark this viewController as initialViewController in Interface Builder. If you have multiple viewControllers in the same Storyboard, you will have to use the instantiateViewController(withIdentifier: _ )

Now, when you need to initialize this viewController, it will be a single-liner:

let homeViewController = HomeViewController.storyboardInstance()

Pretty clear, right?

You can use the same approach for initializing views from nib:

class LoginView: UIView {
     static func nibInstance() -> LoginView? {
if let loginView =
Bundle.mainBundle.loadNibNamed(String.className(self),
owner: nil, options: nil)?.first as?
LoginView {
return loginView
}
return nil
}
}

4. Don’t overload your project with storyboard segues

This will not be the case, if you follow the first advise. But even if you have multiple viewControllers within a single Storyboard, using segues to navigate between viewControllers may be not a good idea:

  • You need to name every segue, that alone is error-prone. Hard-coding the long string names is always a bad programming habit.
  • PrepareForSegue method will become ugly and non-readable, when you add a few segues using either “if/else” or “switch” statements.

What’s the alternative? When you want to navigate to the next viewController on button press, simply add an IBAction for this button, and initialize this viewController in code: it’s actually a single line of code, when you adopt the Advice #3.

@IBAction func didTapHomeButton(_ sender: AnyObject) {
if let nextViewController =
NextViewController.storyboardInstance() {
   // initialize all your class properties
// homeViewController.property1 = …
// homeViewController.property2 = …
   // either push or present the nextViewController,
// depending on your navigation structure
   // present present(nextViewController, animated: true, 
completion: nil)
   // or push  
navigationController?.pushViewController(nextViewController,
animated: true)
}
}

5. Unwind segue? Never heard.

Sometimes the navigation flow should bring user back to the previous screen.

Here is another common mistake: using a new segue to navigate back to the previous viewController. This creates a new instance of the same ViewController, that is already in the view hierarchy, instead of dismissing the top ViewController.

Beginning with iOS 7, Interface Builder gives you the way to “unwind” the navigation stack.

Exit outlet in Storyboard

Unwind segue allows you to specify the destination to go back to the previous screen. It sounds very simple, but in practice it requires some extra steps and only confuses the developer:

  • Normally when you create an action outlet for a button, Interface Builder will create the code for you. In this case Ctrl-dragging from button to the “Exit” outlet is expecting code to already be in your project.
  • Normally when you create an action outlet for a button, it puts the code in the same class that owns the button. For Unwind Segues, you need to write the code in the destination view controller.
  • prepareForUnwind method has all the disadvantages of prepareForSegue method (see the previous advice)

What’s the easier way?

It’s simpler to do it in code: instead of creating an “unwind” action for your button, create a regular IBAction and use dismissViewController or popViewController (depending on your navigation structure):

@IBAction func didTapBackButton(_ sender: AnyObject) { 
// if you use navigation controller, just pop ViewController:  
      if let nvc = navigationController {   
nvc.popViewController(animated: true)
} else {
// otherwise, dismiss it
dismiss(animated: true, completion: nil)
}
}

That’s all for today. I hope, you find something useful for yourself. If you have any comments, questions, or corrections, feel free to contact me.