The fast and simple way of creating constraints in code.

Remember last weeks post? In the article, we created a layout using constraints with only code. Pretty amazing, I know!

But there was one thing that really got me thinking. Usually developing a layout like this is a medium difficult task. In Interface Builder you can easily click together the constraints. In code however, there constraints got a lot more difficult. First there is no visual component, so we need to work of our memory, or the notes we take on our layout. The second thing, and this is the one that shocked me, was the fact that we had about 160 lines of code for that simple layout!

160 FREAKIN’ LINES OF CODE!

This can’t be right!

So today we’ll address this issue. I’m going to show you a way that makes creating constraints in code much simpler and more clear. If we had followed this process in the previous post, we would probably only have had about 100 lines of code. That’s over one third less lines of code!

By the way, I also have another simple way to create constraints in code using a framework called MisterFusion inside of the auto-layout course! This course can help you learn everything about auto-layout in one place instead of piecing your knowledge together.

What we’ll be creating

But before we dive into the code and the technical awesomeness, let’s first take a look at what we’ll be our main goal at the end! Here’s what we’ll be creating:

Pretty cool right?

This layout basically consists of 2 types of rows. The first row, wich is variable in height, and other ones – the one containing 2 views – wich are in fixed height, creating an appealing, alternating pattern.

I’m going to guide you through the process of creating the constraints and also I will try to explain to you, what exactly makes this layout so special. The main focus however will be the almighty „NSLayoutAnchor“. That’s what our main focus will be on.

Introducing NSLayoutAnchor

Let’s just briefly look at what the NSLayoutAnchor is and what it does.

I usually find myself looking up the documentation from Apple since it helps me out a ton. This time is no exception, so let’s look at what it says!

From the Apple documentation:

(NSLayoutAnchor is) … a factory class for creating layout constraint objects using a fluent API.

Use these constraints to programmatically define your layout using Auto Layout. Instead of creating NSLayoutConstraint objects directly, start with a UIView, NSView, or UILayoutGuide object you wish to constrain, and select one of that object’s anchor properties. These properties correspond to the main NSLayoutConstraint.Attribute values used in Auto Layout, and provide an appropriate NSLayoutAnchor subclass for creating constraints to that attribute. Use the anchor’s methods to construct your constraint.

Alright. So basically what I’m reading here is that, with NSLayoutAnchor, I don’t need to create NSLayoutConstraint objects. Instead, I can just apply them to the views that I want them to be applied to and reference the corresponding ones as well. Sounds cool right?

If you look at it from another angle, NSLayoutAnchor basically is a wrapper framework for creating and managing constraints more easily and to create more readable in the code.

Awesome! Now let’s look at how it works!

How to use NSLayoutAnchor

Like I said before, you can just apply NSLayoutAnchors directly to your views that you want to apply them to, whereas before we had to create separate variables and add them to the views.

As an example, before we had to do this:

var constraint = NSLayoutConstraint(item: leftView, attribute: .top, relatedBy: .equal, toItem: topView, attribute: .bottom, multiplier: 1, constant: marginBetweenViews)

What we now can do is call the following function on the view itself to create the same behavior:

leftView.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: marginBetweenViews).isActive = true

What’s happening here is not only the creation of the constraint, but also we’re already adding the constraint to the views. So we’re not only creating the behavior, we’re also activating it in the same step. Isn’t that awesome?

By the way, the activation part is this one right here: .isActive = true

Okay, now to the practical side of things. Let’s get to work!

Developing the Example

Let’s start from common ground this time. I’ve uploaded the starting point for this tutorial right here since I don’t want to dive into all the details of how I added the views to the canvas. You can download the plain Xcode Project right here, or use the buttons at the top of the page. Whatever you like best! 😉

We’re starting with a blank canvas. If you’ve downloaded the project file and opened it up, the simulator should look like this if you run the app:

Now, of course, it won’t stay like that for long! Let’s directly dive into how to add constraints to the first view.

Adding The First Row

Jump into the empty setupFirstRow() function and start with getting the first row and view our of our array of vies that we want to add.

let topView = grid[0][0]

Let’s start by binding all the edges of the view to the edges of the superview. First of, the left boundary:

topView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: marginToBounds).isActive = true

As you can clearly see, we’re taking the view we just got from our array and select the leftAnchor from it. By then entering .constraint() we’re essentially telling the UIView to add a constraint to the selected view. Now it’s time to define the behavior we want to see. For the equalTo: parameter, we’ll select the superviews layoutMargins leftAnchor. As a constant we’re setting our self defined marginToBounds constant, so we can customize the spacing in our app as easily as possible without having to touch a lot of code.

Let’s do the same thing with the right Anchor:

topView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: ( -1 * marginToBounds)).isActive = true

As you can see, we’re multiplying the marginToBounds constant with -1, just like we did in the previous post.

For the topAnchor we’re going to do things slightly differently. The first part stays exactly the same, however, the boundary we select is going to be different. Since the release of the iPhone X, it has become a little more tricky to deal with margins. You can’t simply use the topAnchor, otherwise, stuff is going to get pushed of screen. Instead we’ll need to use the topMargin.

topView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: marginToBounds).isActive = true

We’re not silly using view.topAnchor, but instead we’re using view.layoutMarginsGuide.topAnchor, and that does the trick! Awesome!

Now to finish things up, let’s bind the bottom boundary together, just to create a valid set of constraints for now. We’ll remove this one later.

topView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: (-1 * marginToBounds)).isActive = true

Compile and run the app. You should end up with something like this:

The Pattern

Now to the second row. Things are going to get a little more interesting with the constraints here. We’re getting a little bit more creative.

Let me just briefly explain the concept of the repeating pattern that makes up rows 2 to 4.

As you can see, the pattern is always alternating. There always is a perfect square involved, and the other item takes up the rest of the space. Now this pattern follows a couple of simple rules:

  1. There always is a perfect square in the row.
  2. The rest of the space is a rectangle with the aspect ratio (width : height) of exactly (2 : 1). This means that the width of the rectangle is always exactly double the height of it.
  3. The height of both (rectangle and square) is always equal.
  4. The rows always fill out the entire device width.
  5. The square position always alternates between left and right to create a visual rhythm.

Sounds kind of complex right? I know this can sound intimidating, but we’ll take a look at how to create the behavior we want set by step. We’re going to need to be a little bit creative here and there, but that’s okay. We can make this happen!

Adding The Second Row

First of, we’re going to need all the views that are involved in making the layout happen.

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

The topView is the view from our first row. After that, we’re getting the second row from the array and after that, we’re extracting the views from the row array.

Let’s focus on the simple setup work first. Not much brainwork required here. We’re just binding the left edge of the left view to the boundary. Same with the right edge of the right view.

leftView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: marginToBounds).isActive = true
rightView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: (-1 * marginToBounds)).isActive = true

Next, we’ll bind the views in our row to each other and then to the top view. The only interesting thing here is that we’re binding the top and bottom of the right view to the ones from the left. This makes sure that we avoid conflicting constraints in the future. Now, our left view will define the height of the right view as well as its own height. Pretty clever right? ☺️

leftView.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: marginBetweenViews).isActive = true
rightView.leftAnchor.constraint(equalTo: leftView.rightAnchor, constant: marginBetweenViews).isActive = true
rightView.topAnchor.constraint(equalTo: leftView.topAnchor, constant: 0).isActive = true
leftView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: (-1 * marginToBounds)).isActive = true
rightView.bottomAnchor.constraint(equalTo: leftView.bottomAnchor, constant: 0).isActive = true

This configuration already makes sure 2 of the requirements are met.

I’m ignoring number 5., since this is handled by us, not the auto-layout system! We’re going to code this behavior by hand.

  1. There always is a perfect square in the row.
  2. The rest of the space is a rectangle with the aspect ratio (width : height) of exactly (2 : 1). This means that the width of the rectangle is always exactly double the height of it.
  3. The height of both (rectangle and square) is always equal.
  4. The rows always fill out the entire device width.

Alright. That’s nice. But how do we make sure the other ones are met to? right now, there’s no perfect square in the row, and the aspect ratio of the rectangle is also not set. Also, our constraint system is not really sufficient. The system won’t know how to distribute the space in the row that is left between the two views that are in there. Luckily, once we add the requirements 1. and 2. this problem will be solved!

So let’s get to work!

How can we define the aspect ratio you ask? We’ll let me tell you, there’s just a simple constraint required! Let’s start with the square aspect ratio since this is the easiest!

leftView.widthAnchor.constraint(equalTo: leftView.heightAnchor, multiplier: 1).isActive = true

As you can see, we’re taking the width of our view and setting it equal to the height. And since we’re leaving the multiplier at 1, this will mean that the width will be equal with the height (width : height) -> (1 : 1). Nice!

But still the framework can’t decide how to distribute the available space! We need the second aspect ratio to be defined, so that the height of the row can be calculated! The constraint for the (2 : 1) aspect ratio does look pretty much the same, except for one small but important difference!

rightView.widthAnchor.constraint(equalTo: rightView.heightAnchor, multiplier: 2).isActive = true

We’re setting the multiplier to 2, instead of 1. Also, notice that we’re setting the width of the right view to the height * 2. The order in which the arguments are accessed is important! If we would switch width and height, our view would suddenly be 2 times as high as wide, and that’s certainly not what you’d want out of life! 😬

Now the last thing we need to do for this row, is commenting out the following line in our setupFirstRow() function!

This is what you should end up with:

Cool! We’ve got that behavior of the app down! Now I want you to do something. We have 2 more empty functions in the project. I want you to fill them out so that we have the final result that you can see right here:

I want you to be honest with yourself. I’m going to again append the entire code at the bottom, as well as some tips on what you need to take care of! Only peak into the solution or the tips if you’re feeling completely stuck! Spoiler alert: We don’t need any more constraints to make this happen. You’ve already created and learned all the constraints necessary for this!

If you’re struggling to get things right, I recommend you check out the auto-layout course. This can help you understand a lot of things better and will definitely help you work things out much quicker!

Now go get those last two rows sorted out! 😉 You can do this!

Still hungry for more?

Alright! I hope you found this tutorial helpful. If theres still something missing in your knowledge and you feel like you want to learn more about auto-layout, feel free to check out the course to get your auto-layout knowledge on track.

I’ve spent months preparing and producing this course so you can get your auto-layout knowledge on track within hours! I hope this helps you out!

You can easily copy ALL the code from row 2 and paste it into row 3. There’s only a couple of numbers you need to change. You don’t even need to touch the code itself, you just need to adjust a couple of numbers. In Tip 2, I’ll show you which ones to change!

The numbers you need to change if you copied the code from row 2 to create row 3, are:

let secondRow = grid[2]
let topView = grid[1][0]
...
leftView.widthAnchor.constraint(equalTo: leftView.heightAnchor, multiplier: 2).isActive = true
rightView.widthAnchor.constraint(equalTo: rightView.heightAnchor, multiplier: 1).isActive = true

Of course for readability, I’d recommend you rename secondRow to thirdRow! 😉

If you want to make row 3 work, you’ll also need to comment out this line in the function setupSecondRow():

leftView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: (-1 * marginToBounds)).isActive = true

Row 4 works exactly the same like row 2, so you can copy the code again and you’ll need to adjust 2 numbers:

let secondRow = grid[3]
let topView = grid[2][0]

Of course you’ll again need to comment out the following line in the setupThirdRow() function:

leftView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: (-1 * marginToBounds)).isActive = true
import UIKit

class ViewController: UIViewController {
    let grid: [[UIView]] = [[UIView()], [UIView(), UIView()], [UIView(), UIView()], [UIView(), UIView()]]
    let marginToBounds: CGFloat = 16
    let marginBetweenViews: CGFloat = 8

    override func viewDidLoad() {
        super.viewDidLoad()
        setupViews()
        setupFirstRow()
        setupSecondRow()
        setupThirdRow()
        setupFourthRow()
    }

    func setupViews() {
        var transparency: CGFloat = 1.0
        let gradientValue: CGFloat = (1.0/CGFloat(grid.count + 1))

        for row in grid {
            for singleView in row {
                view.addSubview(singleView)
                singleView.backgroundColor = UIColor(red:1.00, green:0.43, blue:0.43, alpha:1.00)
                singleView.translatesAutoresizingMaskIntoConstraints = false
                singleView.alpha = transparency
            }
            transparency = transparency - gradientValue
        }
    }

    func setupFirstRow() {
        // Retrieve the firt row of the grid and take the one element that's in there
        let topView = grid[0][0]

        topView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: marginToBounds).isActive = true
        topView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: marginToBounds).isActive = true
        topView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: ( -1 * marginToBounds)).isActive = true

        // Make single view work:
        //topView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: (-1 * marginToBounds)).isActive = true
    }

    func setupSecondRow() {
        // Retrieve second row from the array.
        let secondRow = grid[1]
        let topView = grid[0][0]
        let leftView = secondRow[0]
        let rightView = secondRow[1]

        // Pinning to bounds
        leftView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: marginToBounds).isActive = true
        rightView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: (-1 * marginToBounds)).isActive = true

        // Pinning to other views
        leftView.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: marginBetweenViews).isActive = true
        rightView.leftAnchor.constraint(equalTo: leftView.rightAnchor, constant: marginBetweenViews).isActive = true
        rightView.topAnchor.constraint(equalTo: leftView.topAnchor, constant: 0).isActive = true

        // Making the second row work on it's own
        //leftView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: (-1 * marginToBounds)).isActive = true
        rightView.bottomAnchor.constraint(equalTo: leftView.bottomAnchor, constant: 0).isActive = true

        // Define Aspect Ratio of 1:1 -> Square
        leftView.widthAnchor.constraint(equalTo: leftView.heightAnchor, multiplier: 1).isActive = true
        // Define Aspect Ratio of 2:1 -> width = height * 2
        rightView.widthAnchor.constraint(equalTo: rightView.heightAnchor, multiplier: 2).isActive = true
    }

    func setupThirdRow() {
        // Retrieve second row from the array.
        let thirdRow = grid[2]
        let topView = grid[1][0]
        let leftView = thirdRow[0]
        let rightView = thirdRow[1]

        // Pinning to bounds
        leftView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: marginToBounds).isActive = true
        rightView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: (-1 * marginToBounds)).isActive = true

        // Pinning to other views
        leftView.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: marginBetweenViews).isActive = true
        rightView.leftAnchor.constraint(equalTo: leftView.rightAnchor, constant: marginBetweenViews).isActive = true
        rightView.topAnchor.constraint(equalTo: leftView.topAnchor, constant: 0).isActive = true

        // Making the third row work on it's own
        //leftView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: (-1 * marginToBounds)).isActive = true
        rightView.bottomAnchor.constraint(equalTo: leftView.bottomAnchor, constant: 0).isActive = true

        // Define Aspect Ratio of 1:1 -> Square
        leftView.widthAnchor.constraint(equalTo: leftView.heightAnchor, multiplier: 2).isActive = true
        // Define Aspect Ratio of 2:1 -> width = height * 2
        rightView.widthAnchor.constraint(equalTo: rightView.heightAnchor, multiplier: 1).isActive = true
    }

    func setupFourthRow() {
        // Retrieve second row from the array.
        let fourthRow = grid[3]
        let topView = grid[2][0]
        let leftView = fourthRow[0]
        let rightView = fourthRow[1]

        // Pinning to bounds
        leftView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: marginToBounds).isActive = true
        rightView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: (-1 * marginToBounds)).isActive = true

        // Pinning to other views
        leftView.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: marginBetweenViews).isActive = true
        rightView.leftAnchor.constraint(equalTo: leftView.rightAnchor, constant: marginBetweenViews).isActive = true
        rightView.topAnchor.constraint(equalTo: leftView.topAnchor, constant: 0).isActive = true

        // Making the fourth row work on it's own
        leftView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: (-1 * marginToBounds)).isActive = true
        rightView.bottomAnchor.constraint(equalTo: leftView.bottomAnchor, constant: 0).isActive = true

        // Define Aspect Ratio of 1:1 -> Square
        leftView.widthAnchor.constraint(equalTo: leftView.heightAnchor, multiplier: 1).isActive = true
        // Define Aspect Ratio of 2:1 -> width = height * 2
        rightView.widthAnchor.constraint(equalTo: rightView.heightAnchor, multiplier: 2).isActive = true
    }
}
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