Unconventional things you can do with constraints in code.

If there’s a couple of things that I’ve learned in the past few months, it’s that:

  1. You should never place a full tray of delicious muffins in your office, besides your computer. (Seriously don’t!)
  2. The day after burrito day should be the day you choose to do home-office.
  3. Constraints can be created in the most hilarious and creative ways you can imagine if you’re using them in code! (Seriously, you won’t believe what people do with them!)

This weeks post is about the third topic in the list above, though I feel like there is a lot to say on the other items as well.

Anyways: Today we’re going to take a look at creative ways to create constraints, that are not – or hardly – possible in the Interface Builder. Of course, the ways I’ll show you today are wildly creative and have limited use, but they’re always handy to know. To show you what I mean, I have 2 examples prepared that you can download beforehand right here.

Download the sample project and check out the code that I have implied.

Important note: Today we’re not going to develop this layout, but instead I’ll show you how the layouts were created and therefore focus only on a couple of important pieces of code. If you’re interested in learning more about constraints, how they work and what they do, feel free to join the auto-layout course. This course dives in a lot deeper than we will in this post!

Getting creative with constraints

First of, here’s what we have in the sample project:

As you can see the layouts aren’t super pretty this time, but they’re very interesting from the programming side, I promise!

In the layout on the right, we’re using „aspect-ratio type“ constraints to connect the widths of the views in a row together and distribute the space of the view in that way. The interesting part about this is that we never set a width or height and in fact, we do just a little bit of pinning edges. This layout is highly dynamic and very dependent on screen size. You’ll notice that when you rotate the device with the running sample project.

In the layout on the left side, we’re chaining constraints together. We’re always creating the width of a view using the width if the view before it. This is super interesting because we will use loops to create our behavior. This is a type of layout that can easily be refactored so that we don’t need to write a ton of code to create this functionality.

Let’s dive right in!

Aspect Ratios

First, we’ll focus on the easier layout of the two. Take a look at it right here:

Now this doesn’t tell us that much, and that’s why I’ve created this little graphic right here, that shows us what we need to do.

I’ll dive into the details of how the code works in a second, but for now, let’s just look at the concept.

The idea is quite simple, I want the width of the superview to be used 100%. However, the space available should be distributed based on fractions. This means that for example in row 1, the square on the left should take up a certain width. The view on the right, however, should take up 5 times as much space as the one on the left. By setting them to the same height and also defining the view on the left as a square, we get a solvable constraint system.

The only thing that’s different in the second row, is that we change the value of the fractions. The rest is completely the same! We can change the look and feel of a layout by changing a single number. The multiplier of the view on the right.

If you take a loot into the finished project, you can find the code for this layout in the View1Controller.swift file.

The most interesting part of the file is this:

func setupFirstRow() {
        let leftView = views[0][0]
        let rightView = views[0][1]
        rightView.alpha = 0.6
        
        // 1. Pin to Edges
        leftView.topAnchor.constraint(equalTo: superview.topAnchor).isActive = true
        leftView.leftAnchor.constraint(equalTo: superview.leftAnchor).isActive = true
        rightView.rightAnchor.constraint(equalTo: superview.rightAnchor).isActive = true
        
        // 2. Create margin between views
        rightView.leftAnchor.constraint(equalTo: leftView.rightAnchor, constant: marginBetweenViews).isActive = true
        
        // 3. Set same height of row (left view defines height)
        rightView.topAnchor.constraint(equalTo: leftView.topAnchor).isActive = true
        rightView.bottomAnchor.constraint(equalTo: leftView.bottomAnchor).isActive = true
        
        // 4. Aspect Ratio 1:1 for left view
        leftView.heightAnchor.constraint(equalTo: leftView.widthAnchor, multiplier: 1, constant: 0).isActive = true
        
        // 5. Define Aspect ratio for widths
        rightView.widthAnchor.constraint(equalTo: leftView.widthAnchor, multiplier: 5).isActive = true
}

Here’s the step by step rundown of what we’re doing:

  1. We’re pinning our views to the edges. This makes sure our views are taking up the entire width of the view and always stay on the top edge of the view.
  2. We’re introducing margin between the views so that we get a grid-like structure.
  3. It gets more interesting: In this one, we’re making sure the right view always follows the same height and vertical positioning as the left one. We’re doing this because we’re dealing with rows that should have equal heights. This makes sure we don’t get conflicting constraints and everything looks clean.
  4. Now to the first aspect ratio. We’re making sure that the left view in the first row always is a perfect square by setting the width and height of the view to be equal. This means height and width always have the same value.
  5. This is the very interesting part. This is the aspect ratio that creates the rule for width distribution across our 2 views. Remember what I’ve described about the layout? We’re essentially setting the width of the right view to the width of the left view and multiplying it by 5. This results in the right view having 5 times the width of the left one.

Note that in step 5 we’re not giving the width explicit numbers. This means that auto-layout tries to solve the layout by taking a look at all the other involved constraints and tries to figure out if there is a possible solution to this.

Since the height of all the views in the row is equal, the left view has an aspect ratio of 1:1 and the width of the left view is 5:1 with the view on the left, we’re creating a valid set of constraints.

Rundown of what the framework does:

  1. Auto-Layout will figure out the width of the superview thanks to everything we did in 1.
  2. After that it will subtract the margin we’ve introduced 2.
  3. At this point, the height of the row is not yet defined, because the only thing auto-layout knows right now, is that both views should have the same height. This will come in to play later.
  4. Because we have the width of the superview minus the margin we’ve introduced, we can now take 5. and calculate the widths of the views. The framework will take the width of the superview, divide it by 6 and give 5/6 of that to the right view and 1/6 to the left one.
  5. Thanks to 4. we now also have the height of the row and the framework is done.

Now the is just the first row, but the adaptation to the other rows follow the same pattern. It’s just a matter of adopting a couple of simple things in the first function to make sure the second, third and fourth row work as well.

If you’re still unsure about why this works, feel free to check out the auto-layout course right here. That’s where I go in depth about how constraints work.

Chaining Constraints

Alright, that was the first creative layout that I wanted to note. Now if you’ve followed along a couple of tutorials here and there – maybe even some of mine – something might have crossed your mind.

For me, this thought appeared pretty early, because I’m a HUGE fan of making code modular and easy to reuse. Every time I have to type something similar 2 times, I consider storing it in a function to save myself the trouble of making adjustments or even just writing stuff multiple times.

That’s why code like this

// Pin to Edges
leftView.topAnchor.constraint(equalTo: topLeftView.bottomAnchor, constant: marginBetweenViews).isActive = true
leftView.leftAnchor.constraint(equalTo: superview.leftAnchor).isActive = true
rightView.rightAnchor.constraint(equalTo: superview.rightAnchor).isActive = true

// Create margin between views
rightView.leftAnchor.constraint(equalTo: leftView.rightAnchor, constant: marginBetweenViews).isActive = true

// Set same height of row (left view defines height)
rightView.topAnchor.constraint(equalTo: leftView.topAnchor).isActive = true
rightView.bottomAnchor.constraint(equalTo: leftView.bottomAnchor).isActive = true

is a pain to me. Every time I read something like this, my stomach goes mad. I get this feeling that I’m doing something wrong.

While, of course, I’ve skipped some „best practices“ in the last couple of tutorials for the sake of teaching and making stuff easy to learn and understand, I couldn’t help but include some looping for constraint creation in this example.

What we’ll be creating is this:

Doesn’t look like much, but trust me we can make something awesome here. The code for this is a little more complex than the code we had to write before, but as a reward, we don’t have to write as much. The code I’m talking about is the one in the file View2Controller.swift. This file handles the content of the second tabbar item.

Heres another illustration of what this code creates:

Let’s look at the pattern here. You can see, the further we move to the right on the columns, or the further we move down in the rows, the bigger the rows get. More precisely, the columns always have twice the width than the one on their left, the rows twice the size than their above lying counterparts. (Also, notice that again the full width of the view is used!)

That’s the important part here! We’re creating this behavior by chaining constraints together. We’re essentially telling auto-layout to read the width of the first view and multiply it by 2 to set the width of the second view. Then we’re setting the width of the third view to be the width of the second view times 2 which is already two times the width of view one. See how everything is chained together? That’s also why in the illustration I didn’t write „width times 4“ but instead wrote, „width times 2 times 2“. Of course, the result is the same, but this clearly visualizes the chaining of our constraints.

Let’s look at the code that makes this happen:

func setupFirstRow() {
        // 1.
        // retrieve row from array
        let row = views[0]
        // retrieve views from row
        let leftView = row[0]
        let rightView = row[2]
        
        // 2.
        // Pin to bounds of superview
        leftView.topAnchor.constraint(equalTo: superview.topAnchor).isActive = true
        leftView.leftAnchor.constraint(equalTo: superview.leftAnchor).isActive = true
        rightView.rightAnchor.constraint(equalTo: superview.rightAnchor).isActive = true
        
        // 3.
        // Aspect ratio 1:1
        leftView.widthAnchor.constraint(equalTo: leftView.heightAnchor).isActive = true
        
        // 4.
        // Oh yeah! Function action, baby.
        widthCalculation(row: row)
}

func widthCalculation(row: [UIView]) {
        // Setup work
        var previousView = row[0]
        
        // Loop through views in row
        for view in row {
            // 5.
            // Ignore the first view and continue with the second one
            if view.isEqual(previousView) {
                continue
            }
            
            // 6.
            // Set width to be twice the width of the previous view
            view.widthAnchor.constraint(equalTo: previousView.widthAnchor, multiplier: 2).isActive = true
            
            // 7.
            // Create the margin between views
            view.leftAnchor.constraint(equalTo: previousView.rightAnchor, constant: marginBetweenViews).isActive = true
            
            
            // 8.
            // Pin top and bottom to the previous view starting with the second view, so that the first view defines the height of the row
            view.topAnchor.constraint(equalTo: previousView.topAnchor).isActive = true
            view.bottomAnchor.constraint(equalTo: previousView.bottomAnchor).isActive = true
            
            // 9.
            // Making sure we're not always referencing the same previous view. Preparation for the next iteration.
            previousView = view
        }
}

Again, here’s what’s happening:

  1. This is just setup work. We’re retrieving the entire first row from our array. After that, we’ll retrieve the first and the last view of the row. We just need them to bind them to the sides. The rest will be done in a function (that’s why we don’t ahem the middle view right here).
  2. After that, we’re pinning the bounds of our views to the edges so they take up the entire available space in the superview and also so they stay on top all the time.
  3. This is something you should know from the chapter above. We’re just simply setting the aspect ratio of the left view to be a perfect square.
  4. I just want to highlight this: We’re setting up constraints using a function with a loop! That’s got to be worth something!
  5. Since we’re using the function to set up our views, but we already have done everything we need for the first view, we can safely ignore it for now. That’s what we’re doing by using this if statement.
  6. This is essentially the part that chains things together. If you look closely, we have a previous view (the view on the left side of the view we’re currently handling). We’re taking the width of that view and we multiply it by 2. If we do that for all the views we get a chain reaction, setting all the views width based on the first one just with increasing multipliers.
  7. Of course, we want some margin again. Otherwise we couldn’t really see what’s going on right?
  8. This is also something we’ve seen time and time again before. This makes sure that all the views have the same height.
  9. In this step, we’re just preparing for the next iteration of the loop. We just need to update „previous View“ to always have the next view on the left of the view that is currently handled.

Now the pattern is again the same for the following 2 rows. The only big difference there is the fact that we need to set the aspect ratio of 1:1 to the middle view in the second and to the last view in the third row. This makes sure that the heights of the rows increase. Of course, we could also use any other one to create a creative pattern. The sky’s the limit. Go try it out yourself! 😉

I hope you liked this short deep dive into the more complex things about constraints. Also, I hope this sparked your creativity a little bit. I know that constraints can be a tough topic and people seem to not like it very much, but really if you keep an open mind about it and you try to see possibilities instead of issues and problems, you’ll have a really good time working with constraints. And with a little luck and a little practice, it might even be fun sometimes!

If you’re still looking for the shortcut of making constraints work, I warmly recommend you check out the auto-layout course I created. This will definitely help you get rid of a lot of the common struggles and make you an auto-layout master in (almost) no time!

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