Auto Layout using Swift's function builders

Using Swift's function builders to apply Auto Layout constraints | Written by Anthony

Ever since function builders were introduced alongside SwiftUI in Swift 5.1, I’ve been interested in finding a way to put them to use outside of the context of SwiftUI. In essence, function builders are a way to combine a sequence of components into a single value. They are sort of like super-charged reduce functions that can be used to build powerful domain-specific languages (DSLs) like SwiftUI. If you want to understand function builders more deeply, John Sundell did a great job explaining them.

So far, the best use of function builders I’ve seen has been Vadim Bulavin’s custom NSAttributedString function builder, which would have come in handy in my last job when I was building a text editor. Now, thankfully, I’m focused on building a better, more private way to share things with your family, friends, and coworkers, and I don’t need to build a rich text DSL. However, as I’m building our new app, I do find myself building a lot of custom container view controllers, which means I end up writing a lot of code like:

// Create child view controller.
let childViewController = ...

// Add the child view controller to the container.
addChild(childViewController)
view.addSubview(childViewController.view)
		
// Create and activate the Auto Layout constraints for the child’s view.
let constraints = configureConstraints(for: childViewController.view)
NSLayoutConstraint.activate(constraints)
 
// Notify the child view controller that the move is finished.       
childViewController.didMove(toParent: self)

Like most iOS developers, I’ve written a helper extension on UIViewController to help with this.

extension UIViewController {
  func addChildViewController(_ childController: UIViewController) {
    addChild(childController)
    view.addSubview(childController.view)
    childController.didMove(toParent: self)
  }
}

However, this helper function doesn’t include a way to apply Auto Layout constraints, which means I’ve ended up writing a few different varieties of this function that constrain the child’s view to the container’s view or applies an array of constraints passed in as an argument. Although this approach works, it’s not a very elegant API.

ConstraintBuilder function builder

To clean up this API, I decided to create the simplest, most limited function builder possible.

@_functionBuilder
struct ConstraintBuilder {
  static func buildBlock(
    _ constraints: NSLayoutConstraint...
  ) -> [NSLayoutConstraint] {
    constraints
  }
}

All ConstraintBuilder does is take in a variable number of NSLayoutConstraint objects and return them as an array. However, this simple function builder allows us to create a beautiful API for adding a child view controller with constraints.

extension UIViewController {
  func addChildViewController<VC: UIViewController>(
    _ childController: VC,
    @ConstraintBuilder with constraints: (VC) -> [NSLayoutConstraint]
  ) {
    addChild(childController)
    view.addSubview(childController.view)
    childController.view.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate(constraints(childController))
    childController.didMove(toParent: self)
  }
}

Using it is incredibly simple.

let childViewController = ...
addChildViewController(childViewController) {
  $0.view.leadingAnchor.constraint(equalTo: view.leadingAnchor)
  $0.view.trailingAnchor.constraint(equalTo: view.trailingAnchor)
  $0.view.topAnchor.constraint(equalTo: view.centerYAnchor)
  $0.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
}

I’m very happy with this new API for adding child view controllers to custom container view controllers. Let me know what you think.