Cutting down on boilerplate is a good way to make a code base more maintainable and more pleasant to work with.

The NSViewController class supports a variety of layout options. It can be used with XIBs, Storyboards or with views that define their layout in code directly. Additionally there are multiple layout systems: developers might use Auto Layout, Auto Resizing, or both. One tactic for reducing boilerplate is to choose which of these technologies you wish to use, and then simplify NSViewController’s interface down to just the parts you need.

In this post, I’ll define a tiny NSViewController class that we can use as a superclass throughout the rest of a project. My choices are: We’ll write all layout in code, no XIBs or Storyboards at all. Additionally, we will use Auto Layout everywhere instead of Auto Resizing.

With these simplifying assumptions we can now mitigate the following annoyances that we’ll face for every NSViewController subclass we create:

  1. Write a loadView() override which will probably be a boilerplate-y one liner every time.
  2. Access the typed view subclass from the view controller
  3. Gaze upon this designated initializer init(nibName nibNameOrNil: NSNib.Name?, bundle nibBundleOrNil: Bundle?) in all its clunky irrelevance to our layout choices.
  4. Add mandatory boilerplate to implement init?(coder: NSCoder) which exists solely for decoding XIBs… which we are not using at all.

Since the type of NSViewController’s view property is NSView, not the type of our own view subclass, accessing custom methods and properties on our specific view class will be our first problem.

Swift Generics give us a nice way to solve this. We can define the view’s actual type in a new view controller superclass that we’ll use throughout the project.

class XiblessVC<View: NSView>: NSViewController {
  override func loadView() {
    view = View(frame: .zero)
  }
  
  var contentView : View {
    view as! View
  }
}

This provides strongly typed view access, and it saves us having to implement loadView() everywhere. We can also solve the “ugly designated initializer” point with a clean custom initializer to call the designated initializer

  init() {
    super.init(nibName: nil, bundle: nil)
  }

And finally, by annotating the NSCoder initializer using @available, we won’t need to implement this required method in our subclasses.

  @available(*, unavailable)
  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

Here is a quick example of what a view controller might look like if we used NSViewController directly.

class ClockViewController : NSViewController {
  var clockView : ClockView?
  
  init() {
    super.init(nibName: nil, bundle: nil)
  }
  
  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
  
  override func loadView() {
    view = ClockView(frame: .zero)
    clockView = view 
  }
  
  override func viewDidLoad() {
    clockView.date = Date()
  }
}

With XiblessVC as our view controller superclass, we can slim all of that down to something like this:

class ClockViewController : XiblessVC<ClockView> { 
  override func viewDidLoad() { 
    contentView.date = Date()
  }
}

As you can see, the use of this superclass gives us a considerable reduction in boilerplate on every view controller in our project, and encourages consistency with our layout decisions across the project.

There are some drawbacks:

  1. It’s ugly to have a custom view controller superclass used everywhere, rather than using AppKit objects directly.
  2. New developers on the codebase might take a bit longer to pick up what’s going on.
  3. Developers may be tempted add additional features to the superclass, exacerbating the above issues.
  4. Using Generics like this requires the views to have the same access control level as the view controller though, so developers lose the ability to keep custom views private in the same file as the view controller.

I think the trade-off here is worth making though, as long as we keep the superclass simple and focused on our goals: reducing boilerplate and enforcing layout scheme consistency.