Interface Builder makes it very easy to create user interfaces that are driven by Auto Layout. Adding, editing, and deleting constraints is a breeze in Interface Builder. But sometimes you need to work with Auto Layout in code.
Few developers look forward to working with constraints in code. The syntax is verbose and it can be difficult to understand which constraints to add without the visual cues of Interface Builder. Let's be honest, Interface Builder is great for building user interfaces
Setting Up the Project
In this tutorial, we are going to create a form with two text fields and a button. The form is 200 points wide and horizontally centered in its superview. This is the result we are after.
Create a new project in Xcode based on the Single View Application template. Name the project AutoLayoutInCode, set Language to Swift, and Devices to Universal. Tell Xcode where you’d like to save the project and click Create.
Preparing the User Interface
Open ViewController.swift and declare a property for each of the user interface elements of the form, that is, a button and two text fields. Because the user interface elements are positioned and sized by the Auto Layout engine, the frame you pass to the initializer of UITextField
is not important.
import UIKit
class ViewController: UIViewController {
// MARK: - Properties
let button = UIButton(type: .custom)
let textFieldTop = UITextField(frame: .zero)
let textFieldBottom = UITextField(frame: .zero)
}
In the view controller’s viewDidLoad()
method, we configure the user interface elements and add them to the view of the view controller.
override func viewDidLoad() {
super.viewDidLoad()
// Configure Text Field Top
textFieldTop.borderStyle = .roundedRect
view.addSubview(textFieldTop)
// Configure Text Field Bottom
textFieldBottom.borderStyle = .roundedRect
view.addSubview(textFieldBottom)
// Configure Button
button.setTitle("Submit", for: .normal)
button.setTitleColor(.black, for: .normal)
view.addSubview(button)
}
Because we haven’t added any constraints yet, the user interface doesn’t look great. It is time to add constraints for the text fields and the button.
Defining Constraints
Because I don’t want to clutter the viewDidLoad()
method, we implement a few helper methods in which create the constraints for each of the user interface elements. Let us start with the top text field.
We create a new method, generateConstraintsTextFieldTop()
, which returns an array of NSLayoutConstraint
instances. An instance of the NSLayoutConstraint
class represents an Auto Layout constraint.
private func generateConstraintsTextFieldTop() -> [NSLayoutConstraint] {
}
To position and size the text field, we need to create three constraints. Three? Shouldn't we create four constraints?
A text field already knows what its width and height should be. A text field has an intrinsic content size, which means that we don’t need to create explicit constraints for its width and height. But remember that we want the form to be 200 points wide, which means we need to define an explicit constraint to define the width of the text field.
The first constraint we create pins the text field to the top layout guide of the view controller. To create an instance of the NSLayoutConstraint
class, we invoke a pretty length method as you can see below.
let constraintTop = NSLayoutConstraint(item: textFieldTop,
attribute: .top,
relatedBy: .equal,
toItem: topLayoutGuide,
attribute: .top,
multiplier: 1.0,
constant: 28.0)
This method accepts seven arguments. The NSLayoutConstraint
class uses the values of the arguments to create the expression for the constraint. Remember that every constraint can be translated into an expression.
item1.attribute1 [relation] item2.attribute2 * multiplier + constant
With the above expression in mind, the arguments of the method make more sense.
- item on the left side of the expression
- constrained attribute of the left item
- relation between the left and right side of the expression
- item on the right side of the expression
- constrained attribute of the right item
- multiplier of the attribute of the right item
- constant of the attribute of the right item
It makes more sense if we inspect the constraint that pins the top text field to the top layout guide of the view controller. The first argument is the top text field and the attribute, the second argument, is the top edge of the top text field.
The value of the third argument is the relation between the left and right expression, .equal
in this example. The item of the right expression is the top layout guide of the view controller and the attribute is the top edge of the top layout guide.
The multiplier is 1.0
and the constant is 28.0
. This means that the top edge of the top text field is positioned 28 points below the top edge of the top layout guide.
The remaining constraints should now be easier to understand. We center the top text field in its superview, the view of the view controller. This is the constraint that takes care of that.
let constraintX = NSLayoutConstraint(item: textFieldTop,
attribute: .centerX,
relatedBy: .equal,
toItem: view,
attribute: .centerX,
multiplier: 1.0,
constant: 0.0)
The left item of the expression is the top text field and the attribute is its horizontal center. The relation is identical to that of the previous constraint. The right item of the expression is the view of the view controller and the attribute is also its horizontal center. The multiplier is equal to 1.0
and the constant is equal to 0.0
because we want the horizontal centers of the items to align.
The third constraint defines the width of the top text field. This constraint may look a bit odd at first.
let constraintWidth = NSLayoutConstraint(item: textFieldTop,
attribute: .width,
relatedBy: .equal,
toItem: nil,
attribute: .notAnAttribute,
multiplier: 1.0,
constant: 200.0)
The left item of the expression is the top text field and we constrain its width. Because we constrain the size of the top text field, the expression for the constraint doesn’t include a second item. That is why the fourth argument is nil
and the value of the fifth argument is equal to .notAnAttribute
, a special value. The multiplier is equal to 1.0
and the constant is equal to 200.0
, the desired width of the top text field.
We return the constraints as an array at the end of the generateConstraintsTextFieldTop()
method.
private func generateConstraintsTextFieldTop() -> [NSLayoutConstraint] {
let constraintTop = ...
let constraintX = ...
let constraintWidth = ...
return [constraintTop, constraintX, constraintWidth]
}
Adding Constraints
Before we can see the result, we need to add the constraints. This is straightforward, but we need to mindful of a few details. A constraint should always be added to the view that contains both items of the constraint. We need to add the constraints of the top text field to its superview.
If we add the constraints to the top text field, the Auto Layout engine will complain at runtime. Instead, we add the constraints to the view of the view controller, the parent view of the top text field. We pass the array of constraints to the addConstraints(_:)
method, a method of the UIView
class.
view.addConstraints(generateConstraintsTextFieldTop())
We also need to make sure the constraints are added after adding the top text field as a subview to the view of the view controller. It makes no sense to define constraints for a view that isn’t in the view hierarchy. Right?
view.addSubview(textFieldTop)
view.addConstraints(generateConstraintsTextFieldTop())
There is one more detail we need to take care of. If you programmatically add a view to a view hierarchy, the autoresizing mask of the view is automatically translated into Auto Layout constraints. While this is fine in some situations, those constraints can conflict with the constraints we explicitly add to the view.
We need to tell the Auto Layout engine that it shouldn't generate constraints for the autoresizing mask of the view by setting its translatesAutoresizingMaskIntoConstraints
property to false
. This is what the viewDidLoad()
method now looks like.
override func viewDidLoad() {
super.viewDidLoad()
// Configure Text Field Top
textFieldTop.borderStyle = .roundedRect
textFieldTop.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(textFieldTop)
view.addConstraints(generateConstraintsTextFieldTop())
...
}
If you run the application, the top text field should be position below the status bar, horizontally centered in its superview.
Exercise
You should now be able to create constraints for the bottom text field and the button. Take a moment to create the constraints for these user interface elements. You can find the solution below.
Solution
I have created a helper method to create the constraints for the bottom text field. This is what its implementation looks like.
private func generateConstraintsTextFieldBottom() -> [NSLayoutConstraint] {
let constraintTop = NSLayoutConstraint(item: textFieldBottom, attribute: .top, relatedBy: .equal, toItem: textFieldTop, attribute: .bottom, multiplier: 1.0, constant: 8.0)
let constraintLeading = NSLayoutConstraint(item: textFieldBottom, attribute: .leading, relatedBy: .equal, toItem: textFieldTop, attribute: .leading, multiplier: 1.0, constant: 0.0)
let constraintTrailing = NSLayoutConstraint(item: textFieldBottom, attribute: .trailing, relatedBy: .equal, toItem: textFieldTop, attribute: .trailing, multiplier: 1.0, constant: 0.0)
return [constraintTop, constraintLeading, constraintTrailing]
}
Notice that I don’t create a constraint to fix the width of the bottom text field. Instead, the leading and trailing edges of the bottom text field align with those of the top text field. This adds flexibility to the user interface. For example, if you later decide to increase the width of the form to 240 points, you only need to modify the constraint that constrains the width of the top text field.
The constraints for the button look similar, but notice that we don’t define a width for the button. The intrinsic content size of the button takes care of sizing the button. We only align its horizontal center with that of the top text field.
private func generateConstraintsButton() -> [NSLayoutConstraint] {
let constraintTop = NSLayoutConstraint(item: button, attribute: .top, relatedBy: .equal, toItem: textFieldBottom, attribute: .bottom, multiplier: 1.0, constant: 8.0)
let constraintX = NSLayoutConstraint(item: button, attribute: .centerX, relatedBy: .equal, toItem: textFieldTop, attribute: .centerX, multiplier: 1.0, constant: 0.0)
return [constraintTop, constraintX]
}
This is what the viewDidLoad()
method looks like.
override func viewDidLoad() {
super.viewDidLoad()
// Configure Text Field Top
textFieldTop.borderStyle = .roundedRect
textFieldTop.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(textFieldTop)
view.addConstraints(generateConstraintsTextFieldTop())
// Configure Text Field Bottom
textFieldBottom.borderStyle = .roundedRect
textFieldBottom.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(textFieldBottom)
view.addConstraints(generateConstraintsTextFieldBottom())
// Configure Button
button.setTitle("Submit", for: .normal)
button.setTitleColor(.black, for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(button)
view.addConstraints(generateConstraintsButton())
}
And this is what the completed user interface looks like.
What's Next
Working with Auto Layout constraints in code is no rocket science, but it is more verbose and tricker to get right than building a user interface in Interface Builder. At times, however, you have no other option than to create constraints in code.
You have several other options to create constraints in code. You can use Apple's Visual Format Language and you can also leverage layout anchors.
Because working with constraints in code can be tedious, many libraries have emerged over the years that make working with Auto Layout constraints easier and more intuitive.