How to use Nibs and Coordinators instead of Storyboards in Xcode

Canutelo
Canutelo
Aug 23, 2017 · 5 min read

Storyboards in Xcode are convenient at first, but they turn into a bag of hurt as an app grows and needs to be maintained or you work in a team. This article provides guidance based on my own experiences on how to architect apps using nib files instead.

What’s wrong with storyboards

Mobile apps are navigation heavy compared to their desktop counterparts and it makes sense to help developers design and the navigational experience in a visual editor. Apple introduced them in 2010 with Xcode 4 and has made them the default for visual user interface creation over nib files. Sadly there are a lot of problems with Xcode storyboards:

  • Editing them becomes more cumbersome the more screens you add. You have to constantly scroll and zoom around a lot to find that screen you want to edit
  • And if you split up storyboards, it is not that easy to remember on which storyboard you put each view controller
  • They are slow. Each time you open a storyboard it shows a spinner, and Xcode often forgets your last scroll position
  • If you work in a team it is impossible to avoid merge conflicts, and merging storyboard xml is not as easy as merging code
  • If you have a screen that presents different views depending on the apps state, you have to either use containment segues, which means creating a lot of child view controller classes, or stack views on top of each other, which makes editing cumbersome because they are hidden behind each other in Xcode
  • Every so often there is a navigation that is too cumbersome to create with storyboard segues, and you end up doing it in code. Then you have a mix of programmatic and storyboard navigation, which is incosistent and confusing

Creating and using nib files

Since ditching storyboards in favour of nibs my developer life has become so much easier. Thankfully Xcode still lets you create view controller subclasses with an associated nib:

This gives you two files per view controller in your project, right next to each other and always easy to reach:

You can then instantiate it with the default initializer (no identifier string required!):

let viewController = ViewController()

Upon application launch, you then need to assign your root view controller to a window, and tell the application to display that window:

let window = UIWindow(frame: UIScreen.main.bounds)

window.rootViewController = viewConroller

window.makeKeyAndVisible()

(this is something Xcode/iOS conveniently does for us if we specify our main storyboard in the Info.plist, as Xcode’s project templates do)

Typically, you would want to wrap the root view controller inside a navigation controller so you can push other view controllers:

let navigationContoller = UINavigationController(rootViewController: viewController)

window.rootViewController = navigationController

Enter coordinator classes

Architeturally speaking nibs are a perfect fit for coordinators (also known as flow controllers, watch Soroush Khanlou’s excellent introduction talk here). The idea behind it is very simple: You create classes whose single responsibility is to control the navigational flow of your app by managing your view controllers, making storyboard segues obsolete. And whenever you would use UINavigationController.pushViewController(_:animated:) or UIViewController.present(_:animated:) etc. inside your view controller, you call the coordinator instead.

Not only do coordinators allow us to reduce the lines of code in our view controllers, the true strength of this pattern is that is reduces dependencies between view controllers: Two view controllers that are associated by a segue don’t need to know about each other any longer, and they don’t even need to know about the coordinator if we use dependency inversion (i.e. have them talk to their coordinators through a protocol). This is great for our architecture because it means lower coupling and better reusability.

Supplementary views

But back to nib files. One advantage of nib files is that you can put supplementary UIViews on the canvas (unlike storyboards) that are only shown under certain cirumstances, e.g. for when there is no content etc. You can then hook them up to the view controller with an IBOutlet and add them with addSubview() in code. One caveat is that you also need to add constraints at runtime :

(views loaded from a Nib by default convert their autoresizing mask into constraints, which conflicts with our constraints so we want to turn that off:)

mySupplementaryView.translatesAutoresizingMaskIntoConstraints = false

view.addSubview(mySupplementaryView)

(this is the shortest autolayout code I know to constrain a view to its superviews size and position:)

view.addConstraints(NSLayoutContraint.constraintsWithVisualFormat(“V:|[v]|”, options: [], metrics: nil, views: [“v” : mySupplementaryView]))

view.addConstraints(NSLayoutContraint.constraintsWithVisualFormat(“H:|[v]|”, options: [], metrics: nil, views: [“v” : mySupplementaryView]))

mySupplementaryView.isHidden = true

Granted this is a bit of boilerplate, the fact that you can see all your related UI in a nib at a glance without shuffling through subviews makes it easier to maintain it and therefore worthwhile.

View controller containment

You shouldn’t overuse the above trick though. One great technique to reduce view controller code size is to split them up and combine them through view controller containment: You create a parent view controller that is responsible for a single screen and maybe a header and a tab bar, and delegate logical subsections of the screen to child view controllers (in storyboards this can be achieved using an embed segue). The way I prefer to do this is to put child view controllers into designated container views and inherit the size and position throught constraints:

override func viewDidLoad() {

super.viewDidLoad()

let childViewController = ChildViewController()

childViewController.view.translatesAutoresizingMaskIntoConstraints = false

childViewController.willMove(toParentViewController: self)

addChildViewController(childViewController)

containerView.addSubview(childViewController.view)

containerView.addConstraints(NSLayoutConstraint.constraintsUsingVisualFormat(“V:|[v]|”, options: [], metrics: nil, views: [“v” : childViewController.view]))

containerView.addConstraints(NSLayoutConstraint.constraintsUsingVisualFormat(“H:|[v]|”, options: [], metrics: nil, views: [“v” : childViewController.view]))

childViewController.didMove(toParentViewController: self)

Hint: For parent-child relations between view controllers like this I found coordinators not to be useful: The parent can manage its child view controllers fine by itself. For a clean architecture, the child view controllers should talk to the parents though a protocol.

Wether a child view controller or a supplementary view is a better choice depends on the complexity of the main view controller: On one hand you don’t want its code size to grow too much, on the other hand you don’t want to create child view controllers that are don’t have any purpose.

Table and collection views

One drawback of nibs is that you can’t have prototype cells for table and collection views, so you will have to create separate nibs files for each cell class. You could create storyboards with just the table or collection view controller and its prototype cells, the advantage being fewer files. On the other hand you have to create .swift files for each cell class anyways, so the nib files would be easy to find. And having all view controllers in nibs would be more consistent.

)

Canutelo

Written by

Canutelo

Opinions about programming in Swift

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade