I want to start this post with a brief list of points that are often skipped over in tutorials and I think might be useful to people seeking answers:
- Setting constraints programmatically you must set all subviews to subView.translatesAutoresizingMaskIntoConstraints = false (but not self.view). But this rule doesn't apply to views added to a UIStackView (although it does apply to the top level UIStackView).
- The metrics parameter inside the NSLayoutConstraint.constraintsWithVisualFormat() type method, which most tutorials set to nil is a dictionary that can be used instead of placing numbers directly into the strings. For example, we might have a key called "spacing" and set its value to 20 inside the metrics dictionary. Now instead of writing 20 in a visual format string we can write "spacing".
- If you want to work with multipliers then you'll need to use the regular NSLayoutConstraint initializer. Visual Format Language is of no use here (unless you want to reapply constraints on size changes and write the code to do this).
- When using visual format language (VFL) you must set a value for options. No longer can it be nil, as you will find in many tutorials. These NSLayoutFormatOptions are best explained here.
- Responding to size classes programmatically means responding to changes in Size Classes. There are no addConstraintForSizeClass() methods, but there is appearanceForTraitCollection() in the UIAppearance Protocol and there are methods called when size changes: viewWillTransitionToSize() and traitCollectionDidChange().
- In iOS 9 we have new classes for controlling layout: UILayoutGuide, NSLayoutAnchor and NSLayoutDimension. These reduce the amount of code that needs to be written.
- Since iOS 8 you no longer need to add constraints to an "ancestor" view, you simply need to activate them.
Further note: currently when using UIStackView and NSLayoutAnchors in a playground you need to use #availability to check for iOS 9. The compiler will warn you that it's a given that iOS 9 is being used but it's still needed to workaround a current issue the compiler is unaware it's having.
If the Document Outline View is open then you will see a new blue icon appear there, if not then click on the bottom left icon of the storyboard pane to open the Document Outline View. Now open out the blue Constraints icon and select the constraint that has newly been created.
With the the blue icon that is followed by the text "leadingMargin = View.leading" highlighted look now to the Attributes Inspector. This can be opened with alt + cmd + 4 and appears on the right side of the Xcode window.
If we wished to set the priority, we could do so by writing:
So now let's see if we can reproduce this in code.
Storyboard Layout
Let's suppose we drag out a view onto a view controller in a storyboard. And then control drag a constraint to the left side. Releasing the click and selecting "Leading space to container margin" from the list of options that appears.If the Document Outline View is open then you will see a new blue icon appear there, if not then click on the bottom left icon of the storyboard pane to open the Document Outline View. Now open out the blue Constraints icon and select the constraint that has newly been created.
With the the blue icon that is followed by the text "leadingMargin = View.leading" highlighted look now to the Attributes Inspector. This can be opened with alt + cmd + 4 and appears on the right side of the Xcode window.
NSLayoutConstraint
In code this looks like this:let constraint = NSLayoutConstraint(item: self.view, attribute: NSLayoutAttribute.LeadingMargin, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.Leading, multiplier: 1, constant: 0)The item corresponding directly to "First Item" (excluding the dot and what comes after), attribute corresponding to the item after the dot in the first item field, relatedBy corresponding to "Relation", toItem corresponding to "Second Item", attribute corresponding to the item after the dot in the second item field, multiplier corresponding to "Multiplier" and constant corresponding to "Constant".
If we wished to set the priority, we could do so by writing:
constraint.priority = 250Similarly an identifier could be added like so:
constraint.identifier = "my constraint"The constraint is then added to an "ancestor" of the view like so:
self.view.addConstraint(constraint)Or since iOS 8 using one of these two approaches:
NSLayoutConstraint.activateConstraint(constraint) constraint.active = trueIf you repeat the creation of constraints in all three remaining directions, setting each constant to zero, then you will have all the information that we need to place into our code. And your view will look like this in the storyboard:
So now let's see if we can reproduce this in code.
import UIKit class ViewController: UIViewController { // first a function is built to add a view and apply constraints func buildConstraints () { // create subview before creating constraints let subView = UIView() subView.backgroundColor = UIColor(red: 135/255, green: 222/255, blue: 212/255, alpha: 1) // add subview before adding constraints self.view.addSubview(subView) // essential to apply NSLayoutConstraints programatically subView.translatesAutoresizingMaskIntoConstraints = false // trailing margin constraint let const1 = NSLayoutConstraint(item: view, attribute: NSLayoutAttribute.TrailingMargin, relatedBy: NSLayoutRelation.Equal, toItem: subView, attribute: NSLayoutAttribute.TrailingMargin, multiplier: 1, constant: 0) // top constraint let const2 = NSLayoutConstraint(item: subView, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem:topLayoutGuide, attribute: NSLayoutAttribute.Bottom, multiplier: 1, constant: 0) // bottom constraint let const3 = NSLayoutConstraint(item: bottomLayoutGuide, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem:subView, attribute: NSLayoutAttribute.Bottom, multiplier: 1, constant: 0) // leading margin constraint let const4 = NSLayoutConstraint(item: subView, attribute: NSLayoutAttribute.Leading, relatedBy: NSLayoutRelation.Equal, toItem:self.view, attribute: NSLayoutAttribute.LeadingMargin, multiplier: 1, constant: 0) NSLayoutConstraint.activateConstraints([const1, const2, const3, const4]) } override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. self.view.backgroundColor = UIColor.whiteColor() buildConstraints() } }Job done, but there's more than one way to create and apply a constraint.
Visual Layout Format
One of the first things to note before we even start with Visual Layout Format is that "H:|-[view]-|" represents a view with its sides against the margins of the superview, whereas "H:|[view]|" is a view with its edges against the very edge of the superview. There is no similar grammar for the top and bottom layout guides. You must instead retrieve the length property of the guides: "V:|-\\(self.topLayoutGuide.length)-[view]-\\(self.bottomLayoutGuide.length)-|". But don't retrieve the value too early in the life of the view. And as you'll see in the code that follows we can replace the interpolation with a metric if desired.import UIKit class ViewController: UIViewController { func buildConstraints() -> Void { //Initialize let subView = UIView() // This makes view aware of auto layout subView.translatesAutoresizingMaskIntoConstraints = false //Coloring subView.backgroundColor = UIColor(red: 135/255, green: 222/255, blue: 212/255, alpha: 1) //Add them to the view self.view.addSubview(subView) let views = ["subview":subView] let metrics = ["topGuide": topLayoutGuide.length,"bottomGuide": bottomLayoutGuide.length] // Horizontal constraints let horizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("H:|-[subview]-|", options: NSLayoutFormatOptions.AlignAllRight, metrics: metrics, views: views) NSLayoutConstraint.activateConstraints(horizontalConstraints) // Vertical constraints let verticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("V:|-topGuide-[subview]-bottomGuide-|", options: NSLayoutFormatOptions.AlignAllBottom, metrics: metrics, views: views) NSLayoutConstraint.activateConstraints(verticalConstraints) } override func viewDidAppear(animated:Bool) { super.viewDidAppear(animated) // Do any additional setup after loading the view, typically from a nib. buildConstraints() self.view.backgroundColor = UIColor.whiteColor() } }The appeal of the Visual Format Language is that we've been able to reduce four constraints down to two. This is because the strings create an array of constraints with a single string. Where initializing constraints individually we needed to work one by one.
Anchors iOS 9
Using anchors to create constraints is new to iOS 9 and helps keep things concise without the restrictions of VFL.import UIKit class ViewController: UIViewController { func buildConstraints() -> Void { // Initialize let subView = UIView() //Recognize Auto Layout subView.translatesAutoresizingMaskIntoConstraints = false //Coloring subView.backgroundColor = UIColor(red: 135/255, green: 222/255, blue: 212/255, alpha: 1) // Add to the view self.view.addSubview(subView) // add constraints subView.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor).active = true subView.trailingAnchor.constraintEqualToAnchor(view.trailingAnchor).active = true subView.bottomAnchor.constraintEqualToAnchor(bottomLayoutGuide.topAnchor).active = true subView.topAnchor.constraintEqualToAnchor(topLayoutGuide.bottomAnchor).active = true } override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. buildConstraints() self.view.backgroundColor = UIColor.whiteColor() } }
A note on Multipliers
This hasn't been the shortest of posts and there's an accompanying playground, but I want to add a brief note about multipliers. For example, to make our view half the width of the screen in the first NSLayoutConstraint example we'd replace the code for the first constraint with the following:let const1 = NSLayoutConstraint(item: view, attribute: NSLayoutAttribute.TrailingMargin, relatedBy: NSLayoutRelation.Equal, toItem: subView, attribute: NSLayoutAttribute.TrailingMargin, multiplier: 2, constant: 0)And in our iOS 9 anchor example, the following code:
subView.trailingAnchor.constraintEqualToAnchor(view.trailingAnchor).active = trueWould be replaced with:
subView.widthAnchor.constraintEqualToAnchor(self.view.widthAnchor, multiplier: 0.5).active = trueVisual Format Layout (VFL), however, does not support multipliers. This isn't necessarily a reason to abandon VFL, because remember you can mix and match constraint creation. The only thing to be aware of is that you cannot use the anchor approach pre-iOS 9 (and you can't use .active or .activateConstraints() pre-iOS 8).
Conclusion
I'm going to pause there for now, but I have plans to return with a programmatic look at the new UIStackView class (update: now available) and write then about things like centring views and responding to size class changes. You can download an accompanying playground here.
this is an excellent post. I have been pulling my hair out trying to get auto-layout (in code) working.
ReplyDeleteWe've all been there, glad it helped.
DeleteHi, I tried your first code example and the right side and left wasn't equal.
ReplyDeleteI found the problem in this bit of code:
let const4 = NSLayoutConstraint(item: subView, attribute: NSLayoutAttribute.Leading, relatedBy: NSLayoutRelation.Equal, toItem:self.view, attribute: NSLayoutAttribute.LeadingMargin, multiplier: 1, constant: 0)
It was the NSLayoutAttribute.Leading should be NSLayoutAttribute.LeadingMargin
Thanks for this post otherwise!
When using Anchor to set constraints, how to change a constraint later on?
ReplyDeleteFor example, a constraint is set to
(1) subView.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor).active = true
and later, I want to change it to something like
(2) subView.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor, constant: -100).active = true
I try to add (2) directly, but it fails. The console log gives some error about constraint conflict.
What's the right way to do it?
var constraint = subView.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor)
Deleteconstraint.active = true
--------------
constraint.constant = -100
view.layoutIfNeeded()
See also this post.
Thank you! I just didn't realize I had to set translatesAutoresizingMaskIntoConstraints to false to each subview, been trying to figure this out for two days.
ReplyDelete