Effective Auto Layout Programmatically in Swift
Interface Builder (IB) has been in Xcode going back as far as anyone could recall [1]. It initiated the disputes about developing user interface in code vs IB which continue till nowadays. However, with the release of SwiftUI, Apple has made programmatic layout the mainstream approach.
Why hadn’t this happen before? The folklore tells us that constructing UI in code is slower compared to Interface Builder. In this article, I am going to show not only that the opposite is true, but also that programmatic layout is more scalable, maintainable, and future-proof when using the five techniques of Effective Programmatic Auto Layout.
This is not an introductory article. To familiarize yourself with the topic, I recommend Apple’s Auto Layout Guide.
Auto Layout Programmatically vs Storyboard
Let’s look at some facts about using Interface Builder:
- Interface Builder has a low entry barrier.
- Changes are visual.
- To get a full picture of a UI component, you need to inspect both an Interface Builder file and a source code file. Having two sources of truth for a UI component decreases readability and leaves more room for mistakes.
- Too many ways of configuring a component via IB: Runtime Attributes, multiple tabs with different settings, size-class-specific overrides, etc. This makes it difficult to spot which settings are overridden.
- Resolving merge conflicts in storyboards and xibs is error-prone since we have to deal with poorly readable XML. What is more, Interface Builder files can break silently while fixing merge conflicts. Say, you won’t notice a missing
IBAction
connection until you trigger that action. - In complex scenarios, when you need more control over the layout (e.g., screen rotation, variations per screen size or platform, animations, dynamic fonts), you’ll need to resort to programmatic constraints or even manual frame calculation.
Then let’s put some facts about programmatic layout:
- Code is a single source of truth of a UI component.
- View setup is explicit. It is clear which properties are configured and how.
- Merge conflicts are easier to resolve since we are dealing with code rather than XML.
- Extensible for complex scenarios: screen rotation, animations, dynamic fonts, etc.
- The consistent coding style of constructing UI programmatically is maintained across a mixed UIKit and SwiftUI project. With the gradual adoption of SwiftUI, this becomes more and more relevant.
- Less noise in the codebase: no segues, string identifiers, tricky initialization from storyboard and xib, numerous helper extensions attempting to hide some part of that noise.
By looking at these facts we can conclude that the folklore was wrong. Layout in code is faster and more future-proof compared to storyboards and xibs. It is seemingly fast and easy to throw some views together on a canvas. However, in the long run, all of the disadvantages of Interface Builder add up, making it a sub-optimal choice.
To make the programmatic layout even more effective in terms of speed of writing and maintainability, we are going to utilize the five techniques of Effective Programmatic Auto Layout:
- Auto Layout DSL.
- Focused, independent components.
- Callbacks as a communication pattern.
- Stack views over auto layout constraints.
- SwiftUI previews.
Auto Layout DSL
After building UI programmatically for a while, you’ll notice that some compositions are repeated over time. To avoid boilerplate code, I recommend designing a µDSL that provides a thin abstraction layer on top of the excellent layout anchors API.
I consider popular libraries like SnapKit and Cartography a sub-optimal choice. A valuable piece of advice I learned from Ben Sandofsky is to use libraries only for problems outside your domain.
For example, here is the µDSL I’ve been using in my projects. Consider it informative rather than prescriptive. I’ll briefly highlight the key points so that we can see the benefits of using it.
LayoutAnchor
represents a single constraint, which can be either constant (e.g., fixed-width or height), or relative to another constraint (e.g., leading or trailing):
import UIKit
enum LayoutAnchor {
case constant(attribute: NSLayoutConstraint.Attribute,
relation: NSLayoutConstraint.Relation,
constant: CGFloat)
case relative(attribute: NSLayoutConstraint.Attribute,
relation: NSLayoutConstraint.Relation,
relatedTo: NSLayoutConstraint.Attribute,
multiplier: CGFloat,
constant: CGFloat)
}
The factory methods simplify anchors definition:
extension LayoutAnchor {
...
static let leading = relative(attribute: .leading, relatedBy: .equal, relatedTo: .leading)
static let trailing = relative(attribute: .trailing, relatedBy: .equal, relatedTo: .trailing)
static let width = constant(attribute: .width, relatedBy: .equal)
static let height = constant(attribute: .height, relatedBy: .equal)
...
}
UIView
has a convenience method which applies a set of LayoutAnchor
s:
extension UIView {
func addSubview(_ subview: UIView, anchors: [LayoutAnchor]) {
translatesAutoresizingMaskIntoConstraints = false
subview.translatesAutoresizingMaskIntoConstraints = false
addSubview(subview)
subview.activate(anchors: anchors, relativeTo: self)
}
func activate(anchors: [LayoutAnchor], relativeTo: UIView? = nil) {
let constraints = anchors.map { NSLayoutConstraint(from: self, relativeTo: relativeTo, anchor: $0) }
NSLayoutConstraint.activate(constraints)
}
}
extension NSLayoutConstraint {
convenience init(from: UIView, to item: UIView?, anchor: LayoutAnchor) {
...
}
}
Here is how we can put together a couple of views using our µDSL:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let yellow = UIView()
yellow.backgroundColor = .yellow
view.addSubview(yellow, anchors: [.leading(0), .trailing(0), .bottom(0), .top(0)])
let redBox = UIView()
redBox.backgroundColor = .red
view.addSubview(redBox, anchors: [.centerX(0), .centerY(0), .width(100), .height(100)])
}
}
Compare against the standard anchors API:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let yellow = UIView()
yellow.backgroundColor = .yellow
yellow.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(yellow)
NSLayoutConstraint.activate([
yellow.leadingAnchor.constraint(equalTo: view.leadingAnchor),
yellow.trailingAnchor.constraint(equalTo: view.trailingAnchor),
yellow.topAnchor.constraint(equalTo: view.topAnchor),
yellow.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
let redBox = UIView()
redBox.backgroundColor = .red
redBox.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(redBox)
NSLayoutConstraint.activate([
redBox.widthAnchor.constraint(equalToConstant: 100),
redBox.heightAnchor.constraint(equalToConstant: 100),
redBox.centerXAnchor.constraint(equalTo: view.centerXAnchor),
redBox.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
}
If we count the viewDidLoad()
methods, it’s 11 vs 24 lines of code.
Focused, Independent Components
Interface Builder makes it difficult to reuse components. To render a view in the canvas, you’ll need to mark it with the @IBDesignable
attribute and support an alternative lifecycle path via prepareForInterfaceBuilder()
. And even if you manage to do this, the storyboard will load slowly and the view will appear on the canvas only from time to time (this is how it’s been since Xcode 6, and I don’t believe this will ever be fixed).
Conversely, no extra efforts are needed to compose programmatically-written components. Here are the rules of thumb when designing components with reusability in mind:
- Strive for focused, independent views.
- Pass the minimal subset of data the view needs for rendering. Prefer primitive values over view models, business model objects, you name it.
- Do not couple views with protocols or base classes since premature abstraction will likely damage your design. The reason for this is that UI components change at a different speed and for different reasons.
Callbacks as a Communication Pattern
Use callbacks as a primary communication pattern between views. By doing this, we keep construction and configuration code together, and also reduce target-action and delegation boilerplate.
Since iOS 14, we can do this via system APIs. For example, here is how you can attach a callback to UITextField
text changes:
let textField = UITextField()
textField.addAction(
UIAction { action in
let textField = action.sender as! UITextField
print("Text changed \(textField.text)")
},
for: .editingChanged
)
Or instantiate a UIButton
with a callback:
let button = UIButton(primaryAction: UIAction { action in
print("Tapped!")
})
Since most of us don’t have the privilege of supporting iOS 14+, it’s trivial to implement custom callback-based controls. Here is an example for UIButton
:
class CallbackButton: UIButton {
var onAction: ((CallbackButton) -> Void)?
init(onAction: ((CallbackButton) -> Void)? = nil) {
self.onAction = onAction
super.init(frame: .zero)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
private func setup() {
addTarget(self, action: #selector(onTap), for: .primaryActionTriggered)
}
@objc private func onTap() {
onAction?(self)
}
}
The usage is similar to the system API:
let button = CallbackButton { btn in
print("Tapped!")
}
It feels almost like SwiftUI, right?
Here I described SwiftUI View communication patterns.
Stack Views Over Auto Layout Constraints
Stack views provide a layer of abstraction on top of the auto layout, allowing us to compose complex layouts from primitive container views. Everything UIStackView
does is create constraints for us.
If you check Apple’s View Layout overview, the first sentence says:
Use stack views to lay out the views of your interface automatically. Use Auto Layout when you require precise placement of your views.
Even Apple’s Auto Layout Guide introduces stacks before the constraints themselves.
In other words, if we can get away with stack views only, then go for it.
From my experience, stacks are a solution to well over 50% of the problems that Auto Layout was intended to solve. The stack-based layout is faster to build, more concise, readable, and flexible, compared to constraints. What’s more, stacks have several other benefits:
- Trivial to support different content size categories. Just flip the axis of the content if it does not fit.
- Trivial to add scrolling. This is very handy for forms, short lists, and collections, without introducing the overhead of
UITableView
orUICollectionView
. - Multiple ways of aligning and distributing arranged subviews, which are not trivial to achieve via constraints.
- Easy to hide and show arranged subviews from view hierarchy without breaking constraints.
SwiftUI Previews
When building layout in code, it’s often difficult to get to the state where you can verify that the changes that you are making have the results you expect. The goal of SwiftUI Previews is to minimize the amount of time you spend building, and running, and configuring your views to verify the changes that you are making [2].
Although the preview provider supports only SwiftUI.View
, we can seamlessly make UIView
and UIViewController
available for the preview by conforming to UIView(Controller)Representable
[3]:
UIKit with SwiftUI goes into more details on the subject.
import SwiftUI
@available(iOS 13, *)
struct UIViewControllerPreview<ViewController: UIViewController>: UIViewControllerRepresentable {
let viewController: ViewController
init(_ builder: @escaping () -> ViewController) {
viewController = builder()
}
func makeUIViewController(context: Context) -> ViewController { viewController }
func updateUIViewController(_ uiViewController: ViewController, context: Context) {}
}
With the above in place, we are now able to make any instance of UIViewController
conform to UIViewControllerRepresentable
, making it possible to preview our view controller from the previous example:
struct ViewControllerPreviews: PreviewProvider {
static var previews: some View {
UIViewControllerPreview {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
return storyboard.instantiateViewController(identifier: "ViewController") as! ViewController
}
.previewDevice("iPhone SE (2nd generation)")
}
}
Summary
Let’s summarize the five techniques of Effective Programmatic Auto Layout:
- Utilize Auto Layout DSL to cut boilerplate. The example from this article is here.
- Design components with reuse in mind: (a) require a minimal subset of data, (b) focus on doing one thing, (c) not couple to other components via inheritance.
- Communicate between components via callbacks.
- Use stack views to simplify layout and make it fore flexible.
- Use SwiftUI previews for a faster feedback loop.
Thanks for reading!
If you enjoyed this post, be sure to follow me on Twitter to keep up with the new content. There I write daily on iOS development, programming, and Swift.