Storyboards offer a great way to visually layout the views in your scenes and the transitions between them. Their biggest benefit is probably that they provide means to arrange your view hierarchy with direct manipulation. This enables a fast feedback loop where you get an immediate idea of how your view will be rendered at run time.

There are some downsides though. First, storyboards don’t scale very well. Xcode can start to be pretty slow as your storyboard file begins to grow. Second, if you’re working on the same project with other developers, resolving merge conflicts in storyboard files can be extremely difficult. Both of these problems can be improved by splitting storyboard files up as much as possible and connecting them with storyboard references. Even so, such problems can still occur, thus a competent iOS developer need the knowledge on how to write their apps without using any storyboards. In this article, I’m going to give you the basic knowledge required to do just that.

Getting Your App Off Storyboards

The first thing to do in find the main.storyboard file that Xcode automatically generates and delete it. After that, go into the project settings for the app target and remove the text “Main” from General:Deployment Info:Main Interface. We just want this to be blank.

Next we have to manually tell UIKit what view controller we want to start with. We do this in the app delegate method func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool. We need to change it to the following.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
  window = UIWindow(frame: UIScreen.main.bounds)
  window?.rootViewController = MainViewController()
  window?.makeKeyAndVisible()
  return true
}

First, we want to create a UIWindow instance for the window property of our app delegate. Next, we create an instance of our initial view controller and assign it to the rootViewController property on the window. Finally, we call the makeKeyAndVisible method on window to start our view controller.

Creating Our View Hierarchy in Code

Setting up our entire view hierarchy will require a nontrivial amount of code. This code does not belong in our view controllers. We might be tempted to add this to our viewDidLoad or loadView methods, but this is not really a best practice. View controllers should not have the responsibility of setting up the view hierarchy, nor should they know the details of the view hierarchy’s structure. Giving them this information makes them extremely fragile since any change to the UI requires changes to the view controller’s code.

Instead of doing this in our view controllers, we should create a UIView subclass for each view controller who is responsible for creating the view hierarchy. In this view class we will override the init(frame: CGRect) initializer.

override init(frame: CGRect) {
  super.init(frame: frame)

  let label = UILabel(frame: CGRect.zero)
  label.text = "This should be centered."
  label.translatesAutoresizingMaskIntoConstraints = false;

  addSubview(label)
  NSLayoutConstraint.activate([
    label.centerXAnchor.constraint(equalTo: centerXAnchor),
    label.centerYAnchor.constraint(equalTo: centerYAnchor)
    ])
  backgroundColor = .white
}

The first thing we do is call super.init(frame: frame) so UIView can startup with its init. Next, we begin to create our new hierarchy. We’re just going to put a single label in the center of the view. First, we create the label and set its text property. After that, we must set the translatesAutoresizingMaskIntoConstraints to false. Its value is true by default for all views created in code. This property controls how constraints work for the view. If it’s true, the constraints for the view will be automatically created from its frame and autoresizingMask. This is not what we want. We want to manually set its constraints so we must set this to false. Next, we add the label to our view hierarchy by calling addSubview. This step must be done before setting the constraints for the label since constraints can only be added to views that are in the view hierarchy. We call NSLayoutConstraint.activate to create the constraints for the label. Here we’re simply setting its center x and y anchors to match the views center anchors. Finally, we change the backgroundColor of our view to white since it’s black by default. With that, we’re finished setting up our simple view.

Using Our Custom View in Our View Controller

In our view controller, we need to override the method loadView to load our view. The loadView method should not be overridden when using storyboards or xib files to load the view, but since in our case we are using neither, so this is the appropriate place to setup our view.

override func loadView() {
  super.loadView()
  
  view = MainView(frame: CGRect.zero)
}

All we need to do here is call super, create an instance of our custom view class, and assign it to the view property on UIViewController. This way our view controller remains ignorant of the details of our view hierarchy.

Currently, our view is static There is no way to populate it with any custom data so let’s change our code so it allows our view controller to set the text on our label. First we must provide a reference to the label on our view. We’ll add a weak reference to it as a property on our UIView subclass and assign it to the label in init.

weak var label: UILabel!

override init(frame: CGRect) {
  super.init(frame: frame)

  let label = UILabel(frame: CGRect.zero)
  label.text = "This should be centered."
  label.translatesAutoresizingMaskIntoConstraints = false;
  self.label = label
  
  addSubview(label)
  NSLayoutConstraint.activate([
    label.centerXAnchor.constraint(equalTo: centerXAnchor),
    label.centerYAnchor.constraint(equalTo: centerYAnchor)
    ])
  backgroundColor = .white
}

Next, we’ll add the same property on our view controller and assign it to the label reference on the view in loadView.

weak var label: UILabel!
  
override func loadView() {
  super.loadView()
  
  let mainView = MainView(frame: CGRect.zero)
  label = mainView.label
  view = mainView
}

Now, in viewDidLoad we can set the text property on our label.

override func viewDidLoad() {
  super.viewDidLoad()

  label.text = "This is some different text."
}

That’s the basic knowledge needed to create views without storyboards. Happy coding!