How to create constraints in code.

I have to admit. I’m usually more of an Interface Builder user. I know, you might shrug your shoulders right now and note that you don’t care about it, or that you much rather use code and you cant understand why I like the IB so much, but that’s not what I want to focus on this time. After all, if we code, it’s just a matter of getting to the destination. We have these visions in our heads (or sometimes our clients do the „vision having“ part) and the only thing that matters is that those visions come to life. It’s not about the way you get to your goal, it’s the fact that you do end up there. I rarely get asked „How did you do that?“ but more often I hear: „Wow that is just awesome. You made that?“.

Most people are just interested in the what. Only very few people ask for the how.

Sometimes, however, the how is important of making this happen and creating a process that works. For example, if I work with other people in a git, I cant make use of the Interface Builder, because have you ever tried solving a merge conflict with a storyboard file? Yeiks. 😬

That is the exact reason why I decided to focus this week’s auto-layout themed post on creating and handling constraints in code, with just a little visual help from the Interface Builder.

I have prepared this sample file right here, that you can check out, or if you want to maximize your learning effect, you can just skip this and start a Xcode project from scratch. I recommend the starting from scratch option since I’ll be guiding you through every step of the process, but it’s up to you.

Before we dive in, here’s what we will be creating:

In this example, the bottom two rows have two, or three views in them, with each view in a row having exactly the same dimensions than the other ones and they’re all square. This is true for all the views in the bottom 2 rows. The top row will be the main view and should fill up the rest of the available space. If you look closely, the topmost view does not have the same dimensions on the different devices. That’s the interesting part and that’s also the part that makes this layout so very special, amongst some other details.

So let’s jump in and get started, shall we?

Setting Up The Environment

If you’ve downloaded the project file I’ve provided above, you might want to take a look at the sampleView that I’ve set up in the Interface Builder. This is just for demonstration purposes and does already have all the necessary constraints in it. I’ve created it to figure out the best way to create the constraints. If you don’t want that in the project, just delete it, it is not connected to anything.

The first thing we’re going to need is some views. Let’s jump into the ViewController file and start by defining an array of the gird elements. You can take whatever type of view you want, I’d use ImageViews but this time I chose to go the simple route.

let grid: [UIView] = [UIView(), UIView(), UIView(), UIView(), UIView(), UIView()]

We’re going to need an array of a total of 9 views. The first one will be our top view, the following two the second row and the following three after that are going to be the last row.

Of course, this is not a great way of defining a layout, because it’s not dynamic, but let’s focus on the layout for now. 😉

Next, let’s set up all the views and add them to the canvas. We’re going to need to do a couple of things to all of the views, so let’s do that right now.

Create a function called setupViews like this:

func setupViews() {
}

Next, we’ll create a for-each loop in there that will cycle through all the elements in our array of views, so we can apply our custom configuration to them.

func setupViews() {
        for singleView in grid {
            singleView.backgroundColor = UIColor(red:0.22, green:0.56, blue:0.92, alpha: 1.00)
            view.addSubview(singleView)
        }
}

Essentially what we’re doing, is setting the background color of the view to a nice blue tone, and then adding the view to our main view as subviews. We need to do that, in order to let the canvas know that there are some views coming that need to be „drawn“.

We’ll add a call to that function inside of the function viewDidLoad, so that it’ll be called each time the view did load.

override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        setupViews()
        setupConstraints()
}

Let’s run the app for a test!

Voila. As you can see: you can’t see much.

Why is that, you ask? We’ll it’s because our views don’t have a set position and size. If we drag views onto the canvas in Interface Builder, we usually have an intrinsic size and position to make our life easier, but for programmatic views, this is not the case. We need to specify the size and position.

Let’s start by positioning the first view!

Create a function called setupConstraints and a function called setupFirstRow below it. Also, add a call to setupFirstRow to the setupConstraints function. like so:

func setupConstraints() {
    setupFirstRow()
}
func setupFirstRow() {
}

We’re doing this to be able to easily activate or deactivate code once the app gets more complex. This allow for better modularity and makes debugging a hell of a lot easier!

Touching Constraints For The First Time

Now it’s time to get our first view object, set the constraints and add them to the view.

Let’s first get the view in our first row that we want to add to our superview. We can do the by getting it out of the array.

let topView = grid[0]

This is the view we want to add our constraints to. To make our life easier, we’ll store all of our constraints in an array that we can pass to the view later on.

var constraints: [NSLayoutConstraint] =

This is the part where we need to think about what we’re trying to do. Since the nature of the top view is to always stay on top and to always use the entire space available, we can just go ahead and pin the edges to the edges of the superview. This helps us start of simple, and allows for the behavior that we want in the future. Of course we’ll need to modify the layout here and there, but for now this is the best way to go about this!

If you already know how constraints work, this should not be a big deal to you, however I want to quickly note how we can define a constraint. We will use the NSLayoutConstraint object in our case to define the constraints and apply them to our views.

Add this line, behind the equals sign of our array that we just defined:

[NSLayoutConstraint(item: topView, attribute: .top, relatedBy: .equal, toItem: view, attribute: .topMargin, multiplier: 1, constant: 16)]

In this constraint we pin the top edge of the topView to the topMargin of our superview.

I won’t go into too much detail of how the constraints work, because it would crush the scope of this post. If you want to know more about constraints, how they work and what all of those objects are that we entered into the constructor, you should check out the auto-layout module. This is a course I created about auto-layout that dives very deep into the auto-layout system and helps you understand everything you need to know to work with constraints!

The next constraints we’ll need to create are for the left, right and bottom edges of our „topView“. We’ll do this with the following constraints:

constraints.append(NSLayoutConstraint(item: topView, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1, constant: 16))
constraints.append(NSLayoutConstraint(item: topView, attribute: .right, relatedBy: .equal, toItem: view, attribute: .right, multiplier: 1, constant: -16))
constraints.append(NSLayoutConstraint(item: topView, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: -16))

Lastly, add the constraints to the view by adding a line below the constraint creation:

view.addConstraints(constraints)

This code should work for now. Let’s run and test the app.

Most likely, you’ll be disappointed with the result. The reason for that is, that swift will automatically try to convert autorezising masks into constraints. But since we’re defining our constraints ourselves, we don’t want that to happen! let’s turn this of by adding the line: singleView.translatesAutoresizingMaskIntoConstraints = falseto the for-each loop that sets up our views.

Now run again:

BOOM! That looks amazing! Look what you just made! I love this feeling when something works. 😍

Now there’s a small adjustment that I want to make to this code. I don’t like the fact that I have the margin that I want from the edges hardcoded into the constraints. I’d much rather have a variable that I can adjust quickly and effortlessly, so let’s create a constant for that right below our definition of our grid array.

let marginToBounds: CGFloat = 16

Next let’s replace all the constants of our constraints to the newly created constant. For the top and left constraints, the constant can just be replaced by the marginToBounds constant. For the right and bottom sides however we need to multiply the constants by -1, so that the offset is not happening outwards but onwards. This is just a little thing to remember when dealign with constraints. Everything happens relative to the point (0,0) in the top left corner.

The new code should look like this:

var constraints: [NSLayoutConstraint] = [NSLayoutConstraint(item: topView, attribute: .top, relatedBy: .equal, toItem: view, attribute: .topMargin, multiplier: 1, constant: marginToBounds)]
constraints.append(NSLayoutConstraint(item: topView, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1, constant: marginToBounds))
constraints.append(NSLayoutConstraint(item: topView, attribute: .right, relatedBy: .equal, toItem: view, attribute: .right, multiplier: 1, constant: (-1 * marginToBounds)))
constraints.append(NSLayoutConstraint(item: topView, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: (-1 * marginToBounds)))

Adding A Second Row

Now let’s make the layout a little more interesting. Let’s add the second row of views. We’ll start of by creating a new function called setupSecondRow and add the call to our setupConstraints function.

We’ll spend a couple of minutes now on the setupSecondRow function to make things work. Since we’ll not need to do anything else, we’ll spend all our time on this function to make it work.

Let’s first get all the views that we need.

let topView = grid[0]
let leftView = grid[1]
let rightView = grid[2]

Our topView is the view that we just added. leftView and rightView are the two new views that will be added to our layout.

To make things more visually appealing, I chose to set the opacity of the two new views to 70 percent. I did that by adding:

leftView.alpha = 0.7
rightView.alpha = 0.7

Before we add the constraints, I want to be able to control the margin between our rows using a simple constant just like we did with the margins to the superviews bounds. Let’s create a constant for that too, right below our grid array and our marginToBounds constant. let marginBetweenViews: CGFloat = 8

Now to the constraints. Let’s add the following lines of code:

var constraints: [NSLayoutConstraint] = [NSLayoutConstraint(item: leftView, attribute: .top, relatedBy: .equal, toItem: topView, attribute: .bottom, multiplier: 1, constant: marginBetweenViews)]
constraints.append(NSLayoutConstraint(item: rightView, attribute: .top, relatedBy: .equal, toItem: leftView, attribute: .top, multiplier: 1, constant: 0))

Essentially what we’re doing right here, is first creating the array that will contain all of our constraints. The first constraint that we are adding to this array is the one that will keep the space of the top bound of the left view at a constant of 8 to the bottom of the topView. This will create some space between the rows.

Now we want to copy exactly the same behavior to the view on the right. We could just simply swap out „leftView“ with „rightView“ and we would be good to go, but I chose to take a different approach. This is one of the super handy tricks that I’m about to share with you.

I want the top bound of the left view and the top bound of the right view to ALWAYS be in line. That’s why the second added constraint looks a little different. As you can see, we’re setting the top o the right view to be equal to the top of the left view. That way, if the top of the left view changes, the right view will align right with it. The left view will be the „leader“ in this case. This behavior is very useful for layout so keep a sense of an organized layout.

Next, let’s add a line that makes sure the spacing between the two views (leftView and rightView) are also always the same. We’re making use of the marginBetweenViews constant to keep control in our hands.

constraints.append(NSLayoutConstraint(item: rightView, attribute: .left, relatedBy: .equal, toItem: leftView, attribute: .right, multiplier: 1, constant: marginBetweenViews))

We’ll need to make sure the spacing to the bounds is also working:

constraints.append(NSLayoutConstraint(item: leftView, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1, constant: marginToBounds))
constraints.append(NSLayoutConstraint(item: rightView, attribute: .right, relatedBy: .equal, toItem: view, attribute: .right, multiplier: 1, constant: (-1 * marginToBounds)))
constraints.append(NSLayoutConstraint(item: leftView, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottomMargin, multiplier: 1, constant: (-1 * marginToBounds)))
constraints.append(NSLayoutConstraint(item: rightView, attribute: .bottom, relatedBy: .equal, toItem: leftView, attribute: .bottom, multiplier: 1, constant: 0))

The sides are clear. We’ll take the left edge of the left view and the right edge of the right view and align them with the ones from the superview. Pay special attention to the multiplication of -1 on the right side again, like we did it before.

The bottom uses the same principle, as we used it on the top. The left view defines the height of the row and the right view just follows along.

But right now the system would not be able to solve our equations. That’s because the sizes of the views are not properly defined. We’ll solve that by making sure that the aspect ratio of the leftView and the rightView will be 1:1. We can do that with the following code:

constraints.append(NSLayoutConstraint(item: leftView, attribute: .height, relatedBy: .equal, toItem: leftView, attribute: .width, multiplier: 1, constant: 0))
constraints.append(NSLayoutConstraint(item: rightView, attribute: .height, relatedBy: .equal, toItem: rightView, attribute: .width, multiplier: 1, constant: 0))

Lastly, we’ll need to tell auto-layout that the widths of the views in the same row should always be the same.

constraints.append(NSLayoutConstraint(item: rightView, attribute: .width, relatedBy: .equal, toItem: leftView, attribute: .width, multiplier: 1, constant: 0))

Finally, let’s add the constraints to the superview.

view.addConstraints(constraints)

Before we run the app now, we need to comment out the line of code that binds the topView to the bottom edge. We defined the bottom edge of the topView now by setting the size of the second row, so we can get rid of the line by commenting it out. (You can find this in the setupFirstRow function at the very bottom.)

//constraints.append(NSLayoutConstraint(item: topView, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: (-1 * marginToBounds)))

Aaaaand: Boom, again!

The Third Row

This is coming together nicely! The way we do this should now be pretty clear. Let’s add another row for practice, so you can deepen your understanding for what’s going on.

This time, I want you to do this on your own! I won’t give you to much directions on how to achieve this look, but I’ll post the code that makes this happen as well as a couple of tips.

I want you to be honest with yourself. I can’t check on you for this, but try to use this as an exercise to make it happen and learn more about auto-layout. If you’re feeling stuck, feel free to check the tips and the posted code for hints if you’re stuck.

Also, if you’re still unsure how we created all this magic, check out the auto-layout module that covers all of this in greater detail! The module dives deep into how all of this works and why we did things the way we did. You’ll understand why the constructor of a constraint looks the way it looks and you’ll learn that there’s good reasons for doing things the way we did it.

By the way, it’s not magic and I had to learn this stuff just like everybody else. After all, I’m not a wizard!

Here’s what the final result should look like:

Or with some other margins:

Here’s the views that you’ll need to make this happen (plus some nice transparency):

let topLeftView = grid[1]
let leftView = grid[3]
let midView = grid[4]
let rightView = grid[5]

leftView.alpha = 0.5
midView.alpha = 0.5
rightView.alpha = 0.5

 

The total number of constraints added to the third row should be 13. Of course the number may vary, depending on your approach, but fi you follow exactly the same scheme like we used before, 13 is the right amount.

You’ll need to comment out the line of the second row that binds the leftView to the bottom edge of the superview. The exact line I’m talking about is this one:

constraints.append(NSLayoutConstraint(item: leftView, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottomMargin, multiplier: 1, constant: (-1 * marginToBounds)))

Simply comment it out when the third row is done and you should be good to go.

//
//  ViewController.swift
//  ImageGrid
//
//  Created by Ferdinand Göldner on 04.10.17.
//  Copyright © 2017 Ferdinand Göldner. All rights reserved.
//

import UIKit

class ViewController: UIViewController {

    let grid: [UIView] = [UIView(), UIView(), UIView(), UIView(), UIView(), UIView()]
    let marginToBounds: CGFloat = 16
    let marginBetweenViews: CGFloat = 8

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        setupViews()
        setupConstraints()
    }

    func setupViews() {
        for singleView in grid {
            view.addSubview(singleView)
            singleView.backgroundColor = UIColor(red:0.22, green:0.56, blue:0.92, alpha:1.00)
            singleView.translatesAutoresizingMaskIntoConstraints = false
        }
    }

    func setupConstraints() {
        setupFirstRow()
        setupSecondRow()
        setupThirdRow()
    }

    func setupFirstRow() {
        let topView = grid[0]

        var constraints: [NSLayoutConstraint] =
        [NSLayoutConstraint(item: topView, attribute: .top, relatedBy: .equal,
                                              toItem: view, attribute: .topMargin, multiplier: 1, constant: marginToBounds)]
        constraints.append(NSLayoutConstraint(item: topView, attribute: .left, relatedBy: .equal,
                                              toItem: view, attribute: .left, multiplier: 1, constant: marginToBounds))
        constraints.append(NSLayoutConstraint(item: topView, attribute: .right, relatedBy: .equal,
                                              toItem: view, attribute: .right, multiplier: 1, constant: (-1 * marginToBounds)))
        //constraints.append(NSLayoutConstraint(item: topView, attribute: .bottom, relatedBy: .equal,
        //                                      toItem: view, attribute: .bottom, multiplier: 1, constant: (-1 * marginToBounds)))

        view.addConstraints(constraints)
    }

    func setupSecondRow() {
        let topView = grid[0]
        let leftView = grid[1]
        let rightView = grid[2]

        leftView.alpha = 0.7
        rightView.alpha = 0.7

        // Create margins to topView ...
        // ... for left view.
        var constraints: [NSLayoutConstraint] = [NSLayoutConstraint(item: leftView, attribute: .top, relatedBy: .equal,
                                              toItem: topView, attribute: .bottom, multiplier: 1, constant: marginBetweenViews)]
        // ... for right view, use the same height as leftView has. That way, top of leftView always defines the top of the entire row.
        constraints.append(NSLayoutConstraint(item: rightView, attribute: .top, relatedBy: .equal,
                                              toItem: leftView, attribute: .top, multiplier: 1, constant: 0))

        // Create margins between views in same row
        constraints.append(NSLayoutConstraint(item: rightView, attribute: .left, relatedBy: .equal,
                                              toItem: leftView, attribute: .right, multiplier: 1, constant: marginBetweenViews))

        // Create margins to bounds
        // ... to Sides
        constraints.append(NSLayoutConstraint(item: leftView, attribute: .left, relatedBy: .equal,
                                              toItem: view, attribute: .left, multiplier: 1, constant: marginToBounds))
        constraints.append(NSLayoutConstraint(item: rightView, attribute: .right, relatedBy: .equal,
                                              toItem: view, attribute: .right, multiplier: 1, constant: (-1 * marginToBounds)))
        // ... to Bottom
        //constraints.append(NSLayoutConstraint(item: leftView, attribute: .bottom, relatedBy: .equal,
        //                                      toItem: view, attribute: .bottomMargin, multiplier: 1, constant: (-1 * marginToBounds)))
        // Same as for the top. Leftmost view defines the height of the row.
        constraints.append(NSLayoutConstraint(item: rightView, attribute: .bottom, relatedBy: .equal,
                                              toItem: leftView, attribute: .bottom, multiplier: 1, constant: 0))

        // Create equal height and aspect ratio constraints for leftView and rightView
        // Aspect Ratio 1:1 (see multiplier)
        constraints.append(NSLayoutConstraint(item: leftView, attribute: .height, relatedBy: .equal,
                                              toItem: leftView, attribute: .width, multiplier: 1, constant: 0))
        constraints.append(NSLayoutConstraint(item: rightView, attribute: .height, relatedBy: .equal,
                                              toItem: rightView, attribute: .width, multiplier: 1, constant: 0))
        // Equal widths
        constraints.append(NSLayoutConstraint(item: rightView, attribute: .width, relatedBy: .equal,
                                              toItem: leftView, attribute: .width, multiplier: 1, constant: 0))

        view.addConstraints(constraints)
    }

    func setupThirdRow() {
        let topLeftView = grid[1]
        let leftView = grid[3]
        let midView = grid[4]
        let rightView = grid[5]

        leftView.alpha = 0.5
        midView.alpha = 0.5
        rightView.alpha = 0.5

        // Create margins to topView ...
        // ... for left view.
        var constraints: [NSLayoutConstraint] = [NSLayoutConstraint(item: leftView, attribute: .top, relatedBy: .equal,
                                                                    toItem: topLeftView, attribute: .bottom, multiplier: 1, constant: marginBetweenViews)]
        // ... for mid view.
        constraints.append(NSLayoutConstraint(item: midView, attribute: .top, relatedBy: .equal,
                                              toItem: leftView, attribute: .top, multiplier: 1, constant: 0))
        // ... for right view.
        constraints.append(NSLayoutConstraint(item: rightView, attribute: .top, relatedBy: .equal,
                                              toItem: leftView, attribute: .top, multiplier: 1, constant: 0))

        // Create margins between views in same row
        constraints.append(NSLayoutConstraint(item: midView, attribute: .left, relatedBy: .equal,
                                              toItem: leftView, attribute: .right, multiplier: 1, constant: marginBetweenViews))
        constraints.append(NSLayoutConstraint(item: rightView, attribute: .left, relatedBy: .equal,
                                              toItem: midView, attribute: .right, multiplier: 1, constant: marginBetweenViews))

        // Create margins to bounds
        // ... to Sides
        constraints.append(NSLayoutConstraint(item: leftView, attribute: .left, relatedBy: .equal,
                                              toItem: view, attribute: .left, multiplier: 1, constant: marginToBounds))
        constraints.append(NSLayoutConstraint(item: rightView, attribute: .right, relatedBy: .equal,
                                              toItem: view, attribute: .right, multiplier: 1, constant: (-1 * marginToBounds)))
        // ... to Bottom
        constraints.append(NSLayoutConstraint(item: leftView, attribute: .bottom, relatedBy: .equal,
                                              toItem: view, attribute: .bottomMargin, multiplier: 1, constant: (-1 * marginToBounds)))
        constraints.append(NSLayoutConstraint(item: midView, attribute: .bottom, relatedBy: .equal,
                                              toItem: leftView, attribute: .bottom, multiplier: 1, constant: 0))
        constraints.append(NSLayoutConstraint(item: rightView, attribute: .bottom, relatedBy: .equal,
                                              toItem: leftView, attribute: .bottom, multiplier: 1, constant: 0))

        // Create equal height and aspect ratio constraints for leftView and rightView
        // Aspect Ratio 1:1 (see multiplier)
        constraints.append(NSLayoutConstraint(item: leftView, attribute: .height, relatedBy: .equal,
                                              toItem: leftView, attribute: .width, multiplier: 1, constant: 0))
        // Update, 24.10.2017: Actually you don't need these two constraints. That is because, we matched the top and bottoms of the views to the leftView. Therefore the views all have the same height. Thanks to Neal for pointing that out to me!
        // constraints.append(NSLayoutConstraint(item: midView, attribute: .height, relatedBy: .equal,
        //                                       toItem: midView, attribute: .width, multiplier: 1, constant: 0))
        // constraints.append(NSLayoutConstraint(item: rightView, attribute: .height, relatedBy: .equal,
        //                                       toItem: rightView, attribute: .width, multiplier: 1, constant: 0))
        // Equal widths
        constraints.append(NSLayoutConstraint(item: midView, attribute: .width, relatedBy: .equal,
                                              toItem: leftView, attribute: .width, multiplier: 1, constant: 0))
        constraints.append(NSLayoutConstraint(item: rightView, attribute: .width, relatedBy: .equal,
                                              toItem: leftView, attribute: .width, multiplier: 1, constant: 0))

        view.addConstraints(constraints)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

}

I hope you enjoyed this tutorial!
If you have any questions, feel free to hit me up on email or check out the auto-layout module!

Ask Anything!

Do you have any questions? Ask them! I'll answer via Email as soon as I read it.

Not readable? Change text. captcha txt
0