Fear and Loathing in Auto Layout: Programmatic Constraints


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:
  1. 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).
  2. 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".
  3. 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).
  4. 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.
  5. 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().
  6. 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. 
  7. Since iOS 8 you no longer need to add constraints to an "ancestor" view, you simply need to activate them.
Note: I hit a problem in playgrounds when trying to use anchors with layout guides. Top and bottom layout guides don't mean much in a playground because there's no status bar, but they are acceptable to use when creating constraints in the older ways. It looks like a bug that the anchor approach doesn't support them (or at least not in the same way as it supports them in a regular app).

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.

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 = 250
Similarly 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 = true
If 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 = true
Would be replaced with:
subView.widthAnchor.constraintEqualToAnchor(self.view.widthAnchor, multiplier: 0.5).active = true
Visual 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.

Comments

  1. this is an excellent post. I have been pulling my hair out trying to get auto-layout (in code) working.

    ReplyDelete
  2. Hi, I tried your first code example and the right side and left wasn't equal.

    I 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!

    ReplyDelete
  3. When using Anchor to set constraints, how to change a constraint later on?

    For 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?

    ReplyDelete
    Replies
    1. var constraint = subView.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor)
      constraint.active = true
      --------------
      constraint.constant = -100
      view.layoutIfNeeded()

      See also this post.

      Delete
  4. 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

Post a Comment