Bridging Auto Layout with frame based layout

Have you ever run into the issue of placing your new Auto Layout based view within some older frame-based code? One problem you’ll encounter is that you need a way for the frame-based layout to know when its Auto Layout subviews change in size. It turns out that this can be solved in a pretty straight-forward way! In this post I’ll run through an example as well as the solution.

Let’s start with an example: our parent UIViewController creates an AutoViewController whose size is defined by Auto Layout constraints. Internally, our AutoViewController holds a UILabel bound to its edges. As the number of lines of text changes, the AutoViewController will expand to accomodate this.

private let autoVC = AutoViewController()
override func viewDidLoad() {
super.viewDidLoad()
addChildViewController(autoVC)
view.addSubview(autoVC.view)
}

We’ll need a way of finding the size of the AutoViewController so that we can set its frame. Using the systemLayoutSizeFitting(targetSize:) method on UIView, we can obtain the height of the subview as defined by Auto Layout. In our case, we want the view to appear as a fixed-width row within the parent, so we’ve defined a method to obtain the size. We provide the available width but ask for a compressed height — otherwise our Auto Layout view is free to stretch vertically more than it needs to, filling up the parent.

private func rowSize(forView view: UIView) -> CGSize {
return view.systemLayoutSizeFitting(
CGSize(width: view.bounds.width,
height: UILayoutFittingCompressedSize.height)
)
}

We can then use this method to set the frame of our Auto Layout based view. Though it’s not strictly required, we do this as the parent goes to lay out its subviews. Ultimately this view will be one part of a more complex frame-based layout, so if it changes, its sibling views would likely need to adjust too. viewWillLayoutSubviews() is a good place to orchestrate this.

override func viewWillLayoutSubviews() {
let childFrame = CGRect(
origin: .zero,
size: rowSize(forView: autoVC.view)
)
autoVC.view.frame = childFrame
}

We’re still left with a problem. Our Auto Layout based child UIViewController may change its size. Here’s what that looks like:

Thankfully, Apple gives us a way to know when this happens. By handling systemLayoutFittingSizeDidChange(forChild:) we can respond to this. Here, we mark the parent view as needing a layout pass which will do the trick, since the frames of our children are set within viewWillLayoutSubviews.

override func systemLayoutFittingSizeDidChange(forChildContentContainer: UIContentContainer) {
view.setNeedsLayout()
}

That’s it! I like this solution since it avoids having your UIView/ UIViewController classes explicitly communicate size changes to their parent — a method which is often error-prone and can lead to infinite layout passes.