Autolayout is here to stay. It’s the best tool we have for dealing with wanting to configure just one interface and present it in different real estates, orientations and size classes. I believe most of us will agree to that simple statement.

Something that not everyone is accustomed to is to also cover different states of the app with Autolayout. Meaning by that that a button -or label, or any other element- may have a leading space of 8px to container under some set of circumstances and 40px under a different ones.

I placed a project on GitHub that I’ll reference throughout this post.

All four screens are basically the same, the main difference between them are on the way constraints are manipulated in the changes closure declared on the configurePosition() function.

Constants

Like almost every other element in Interface Builder, you can create an IBOutlet to a constraint. The easiest and most straight forward way to modify a constraint is by changing its constant value.

Let’s say you want to have a label that has a leading of either 8 or 40 pixels to its containing view depending on the value of a UIPicker.

Once you have a label setup with the default autolayout constraints laid down, select the label to display its constraints, control + drag from the leading constraint into your class to create an outlet. You can also do so by expanding the label’s container view’s constraints and locating the leading one in the document outline.

Outlet

All you need to do now is to change that property’s constant from 8 to 40, and back depending on the value of the position property.

extension LeadingViewController {
	
  private func configurePosition() {
    ...

    let changes = {

      self.constraint.constant = self.position == .Left ? 8 : 40
      self.view.layoutIfNeeded()
    }
  }
}

A full example of this behaviour can be found on the project’s LeadingViewController.

LeadingViewController

More Complex Changes

That was pretty straight forward because we are always anchored to the left. Let’s now say that we want the label to be either aligned left or right in the screen, both times with a margin of 8px.

We could still use the same strategy and do some math. Now, the changes closure would look something like this:

let changes = {

  let leftPosition: CGFloat = 8.0
  let rightPosition: CGFloat = self.view.bounds.width - self.positionLabel.bounds.width - 8.0
  self.positionLabelLeadingConstraint.constant = self.position == .Left ? leftPosition : rightPosition

  self.view.layoutIfNeeded()
}

It’s not as clear but it still does the job. A full example of this behaviour can be found on the project’s BorderViewController.

BorderViewController

Even More Complex Changes

We now want our label to be left, center or right aligned. Strap yourselves in, this will get messy.

let changes = {
    
    var leadingConstraint: CGFloat = 0.0
    
    switch self.position {
        
    case .Left:
        
        leadingConstraint = 8.0
        
    case .Center:
        
        leadingConstraint = (self.view.bounds.width - self.positionLabel.bounds.width) / 2.0
        
    case .Right:
        
        leadingConstraint = self.view.bounds.width - self.positionLabel.bounds.width - 8.0
    }
    
    self.positionLabelLeadingConstraint.constant = leadingConstraint
    
    self.view.layoutIfNeeded()
}

Still kinda works and your coworkers or future self will hate you for doing this. A full example of this behaviour can be found on the project’s FullViewController.

FullViewController

A Different Approach

You probably see where this is going. All this math is error prone, hard to maintain and hard to understand. There is a better way.

What we are going to do is add three different constraints, one that will cover each state and bind them to the leftPositionConstraint, centerPositionConstraint and rightPositionConstraint properties.

The left constraint will have a leading horizontal space to container view with a constant of 8px.
The center constraint will center horizontally in container.
The right constraint will have a trailing horizontal space to container view with a constant of 8px.

Of course, these three constraints can’t be satisfied simultaneously… unless we change their priority.

So what we will do is set the left constraint priority (initial state) to UILayoutPriorityDefaultHigh(750) and the center and right constraints priorities to UILayoutPriorityDefaultLow(250).

You should end up with something like this (note that non-required constraints are displayed as dashed lines):

Constraints

To set a constant priority to high or low, simply select the option from the constants priority dropdown in Interface Builder.

The Reward

Now, our code will be less error prone, easier to understand and maintain.

let changes = {
    
    switch self.position {
        
    case .Left:
        
        self.leftPositionConstraint.priority = UILayoutPriorityDefaultHigh
        self.centerPositionConstraint.priority = UILayoutPriorityDefaultLow
        self.rightPositionConstraint.priority = UILayoutPriorityDefaultLow

    case .Center:

        self.leftPositionConstraint.priority = UILayoutPriorityDefaultLow
        self.centerPositionConstraint.priority = UILayoutPriorityDefaultHigh
        self.rightPositionConstraint.priority = UILayoutPriorityDefaultLow

    case .Right:

        self.leftPositionConstraint.priority = UILayoutPriorityDefaultLow
        self.centerPositionConstraint.priority = UILayoutPriorityDefaultLow
        self.rightPositionConstraint.priority = UILayoutPriorityDefaultHigh
    }
    
    self.view.layoutIfNeeded()
}

Mutability

If you plan on modifying a constraint’s priority on execution, you need to make it mutable. Make sure that none of your dynamic constraints have a priority of UILayoutPriorityRequired(1000). Otherwise you’ll end up with crashes like the following:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException',
reason: 'Mutating a priority from required to not on an installed constraint (or vice-versa) is not supported.
You passed priority 250 and the existing priority was 1000.'

Activate/Deactivate Constraints

On a final note, NSLayoutConstraint provides us with API to activate(_:) and deactivate(_:) constraints, but this is sadly unavailable from Interface Builder, and thus, prevents us from having a working, warning free interface that holds multiple constraints.

It should be the best approach if you are working with constraints in your code and not in Interface Builder.

Next

Addendum: Dynamic Autolayout + ReactiveCocoa, making this even simpler with FRP and ReactiveCocoa.