The Ultimate Guide to iOS AutoLayout (IV): Demystifying NSLayoutConstraintMarch 26, 2017 in
By now, you probably think you are very proficient at AutoLayout, and you are certainly right. However, to be a complete AutoLayout ninja, you need to master NSLayoutConstraint. In the 4th episode of “The Ultimate Guide to iOS AutoLayout”, we are demystifying NSLayoutConstraint.
In the previous episode of the series, you learned about size classes, and how to build adaptive interfaces for all of your iOS Devices. We also learned that sometimes it’s necessary to “soil your hands” by modifying constraints programmatically.
In this episode, you’ll learn everything about NSLayoutConstraint.
Is it necessary to know how to manage constraints programmatically? Absolutely.
Some time ago, I worked in BioProfe, an application that displays exams with exercises for students. It includes a series of complex formulae that need to be read from streams and displayed.
I had to position all the elements contained in the formula in an AutoLayout environment. Take into account that a division, for example, may have infinite levels of vertical layers top and bottom. Also, users were supposed to interact with some of the elements (i.e: buttons with unknown values). Furthermore, StackViews didn’t exist at the time.
I wouldn’t be able to solve this situation by just defining constraints in IB and adjusting some constants. I needed to deeply understand how to build a set of constraints in code.
You might find yourself in front of a similar challenge. A good knowledge of NSLayoutConstraint will be essential in these cases.
NSLayoutConstraint is the class representation of a visual constraint from IB (actually, the opposite is more correct). You can think on it as an object that relates an view with a dimension (height, width) or another view, via a property (top, center, height…).
A NSLayoutConstraint, conceptually, has the following properties:
- item: the view this constraint is applied to (mandatory)
- attribute: the relevan attribute of the “item” this constraint is affecting. Must be an instance of the NSLayoutAttribute enumeration (i.e: .width, .height, .centerX, .trailing, etc).
- relatedBy: the relationship that’s specified by this constraint. Must be an instance of the NSLayoutRelation enumeration, i.e: x is .equal to y, a is .greaterThanOrEqual than b.
- toItem: if the constraint relates two objects, this is the second object.
- toAttribute: if the constraint relates two objects, this is the attribute that’s being referenced for the second object (toItem). Must also be an instance of the NSLayoutAttribute enumeration.
- multiplier: a multiplier that relates the “relatedBy” relationship between the attributes from the two objects.
- constant: an offset that relates the “relatedBy” relationship between the attributes from the two objects, applied as a sum after the multiplier.
Don’t worry if this seem like a lot of information. You’ll understand it easily with some examples:
Example 1: setting a view’s fixed height
First, this example will show you how to define a constraint that only affects one view. Let’s say that we want to set the width of a constraint to 150px. We will create an NSLayoutConstraint like this:
NSLayoutConstraint(item: ourView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1, constant: 150)
We could describe this definition as:
create a constraint for ourView to set its width equal to a fixed attribute of 1 x 150px.
Notice how we did set “toItem” as nil, as we are not relating this view with any other view.
Example 2: a view’s width is twice another view’s height
Let’s say we want to specify that a view’s width has to be set to twice the height of another view. The following figure describes this scenario.
The correct way of defining this constraint will be:
NSLayoutConstraint(item: view2, attribute: .width, relatedBy: .equal, toItem: view1, attribute: .height, multiplier: 2, constant: 0)
Note that we are not defining the rest of constraints for these views. Thus, the rest of properties for the views that appear in the figure should need to be defined too.
Where to add the constraint
Now that we know how to define constraints programmatically, we need to how to add those constraints into our view hierarchy. We add individual constraint to a view by means of the addConstraint: method of UIView.
The rule of thumb is: constraints that affect only one view are added to that view. Constraints that affect two or more views need to be added to their parent view.
Let’s see how the constraint of the first example could be added to its view:
let c = NSLayoutConstraint(item: ourView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1, constant: 150) myView.addConstraint(c)
Now, let’s see how we would add the constraint to view1 and view2 in our second example. Let’s suppose that both are
let parentView = UIView(...) let view1 = UIView(...) parentView.addSubview(view1) let view2 = UIView(...) parentView.addSubview(view2) let c = NSLayoutConstraint(item: view2, attribute: .width, relatedBy: .equal, toItem: view1, attribute: .height, multiplier: 2, constant: 0) parentView.addConstraint(c)
Of course, this means that two views cannot be related via a NSLayoutConstraint unless they belong to the same view hierarchy’s layer (i.e: they have same parent view).
Defining constraints with visual format
The visual format is an additional way of defining constraints for a view. It’s power lies in the fact that you can easily define multiple constraints with a simple sentence. This visual language is nothing more than a string following a set of conventions.
To define constraints with the visual language, we use the NSLayoutConstraint.constraints(withVisualFormat:options:metrics:views:) method from NSLayoutConstraint. This returns an array of instances of NSLayoutConstraint, that we can add to a view by means of the view.addConstraints(…) method.
constraints(withVisualFormat:options:metrics:views:) needs you to specify the views that are referenced in the visual format as a dictionary. This dictionary will match the “name” (string representation) of the view and the UIView object, so when you refer to one of those views by its name in the visual format string, the system will know the corresponding UIView’s object.
Let’s see an example:
NSLayoutConstraint.constraints(withVisualFormat: "V:|-[myview]-|", options:, metrics: nil, views: ["myview": myView])
Here, the visual format is “V:|-[myview]-|”. First, you see a [myview] annotation, that points to a view called “myview”. In the “views:” section we have a dictionary where we specify that this annotation refers to “myView”. myView is an instance of UIView. Thus, each time [myview] appears in the visual format, we are defining a constraint that affects myView.
Basic rules of the visual format
Hence, let’s see the basic rules of the visual language:
- Views are defined, as we already shown, with a name wrapped in brackets, like “[myview]” in the example.
- Axis: constraints defined with visual format are uni-dimensional (you either define vertical constraints or horizontal constraint, but not both in a single rule). By default, the constraints defined are horizontal. If you want to define vertical constraints, you must prefix the visual format with “V:”. You can also specify horizontal constraints explicitly with the “H:” prefix.
- To specify edges, we use a pipe symbol “|”. Edges appear optionally at the beginning or end of the visual format. In a vertical axis, they are the top and bottom edges respectively. In an horizontal axis, they are the left and right edges.
- We define distances with the hyphen symbol, “-” . A single hyphen means the standard distance (8px). However, to specify a custom distance, you must add two hyphens wrapping the distance in pixels. I.E: this visual format “[view1]-25-[view2]” means that view1 must be 25 pixels at the left of view2.
- We define size constraints by adding them inside the view in parenthesis, prefixed by the size relationship (greater than or equal: “>=”, equal: “==”, less than or equal: “<=”). I.E: To specify that a view1 must have a height of 50 pixels or less, the visual format will be “V:[view1(<=50)]”
- Equality relationship between views are also specified by “==”, “<=” and “>=”. In this case, instead of specifying a constant value, you specify the other view, i.e: “[view1(==view2)]”
- Finally, we specify constraint priorities with the @priority notation, where “priority” is a number containing the value for the priority.
Let’s see some examples taken from the Apple visual format appendix:
First, a button and a textfield separated by the standard distance (8px):
A button with a width of 50 pixels or more:
A view that’s positioned exactly 50px to the left and right edges of its superview.
Next, two textfields separated 10px vertically:
Two views that are one at the left of the other, with no separation between them.
Then, a width constraint for a button with a priority value of 20:
Two buttons that have the exact same width:
A button that has a width greater or equal to 70 and less or equal than 100.
Finally, three elements in a row. A “find” button, a “Find next” button and a textfield. The two buttons are separated by the standard 8px, while the button and the textfield are separated by 20px. The first button pins to the left edge, and the textfield pins to the right edge of its superview.
Adding the visual format constraints to a view
Remember how we learned that constraints that relate two or more views need to be specified in the parent view? This applies to the constraints defined using a visual format string. This is because NSLayoutConstraint.constraints(withVisualFormat:options:metrics:views:) returns an array of constraints.
Now, let’s see how to properly define and add constraints to a view in visual format:
let containerView = UIView() let upperFill = UIView() containerView.addSubview(upperFill) let leftFill = UIView() containerView.addSubview(leftFill) let rightFill = UIView() containerView.addSubview(rightFill) containerView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-[left]-[upper]-[right]-|", options: , metrics: nil, views: ["upper": upperFill, "left": leftFill, "right": rightFill]))
This constraint distributes the views in a row pinned to the edges, left to right, in its superview, containerView.
First, we define our containerView, that is the common superview for the other views. Then we add those views (upperFill, leftFill and rightFill) to containerView. Finally, we define and add the constraints visually to the containerView view. That is the superview of all the views included in the visual format.
You’ll notice how the views’ dictionary contains the names for the views and their references. The first one is “upper” for the upperFill view. Thus, in “H:|-[left]-[upper]-[right]-|”, “[upper]” refers to the upperFill view.
When to define constraints in source code
For most simple applications, you will probably be able to define your user interfaces in IB. However, there are situations in which a more specialized approach is required.
Usually, when you need to add elements to a view dynamically in runtime, you will need to use the techniques we learned today. Some use case scenarios are:
- If you add views in response to user interactions, like a game or a puzzle app.
- When views are visual representations of some data retrieved from the network. These views will need to arrange as they are read and parsed. That was my case with the BioProfe app.
- When you generate views dynamically in response to time or data events, and these views need to arrange in a certain way.
Where to go next
In this episode of “The Ultimate Guide to iOS AutoLayout”, we explored the possibilities of NSLayoutConstraint. This class allows us to define constraints programmatically. This is a powerful feature, specially when using the visual format, and allows us to define pretty complex sets of constraints.
Defining constraints programmatically will allow you to address some AutoLayout configurations that need to be defined or grow dynamically.
In the Digital Tips list, I will also share with you some tips to magically align views vertically or horizontally within a superview.
In the next episode of the series, we’ll learn how to properly animate views in an AutoLayout world.