UX Collective

We believe designers are thinkers as much as they are makers. https://linktr.ee/uxc

Follow publication

The why, what, and how of Swift Auto-Layout

--

I think people are a lot like UI components on a phone. In a way, we’re all trying to find our place in this bezel-less screen we call life.

We want to find where we’re meant to be in this world, where we can grow and fill the space around us with our potential, or where we can settle to make room for others.

We might not ever fill these longings in life, but at the very least, we can find solace in knowing that iOS components using Auto-Layout have found their place in theirs.

We’re going to take a look at why Auto-Layout exists, how it works, and some ways we can use it to configure dynamic UI (before things like VStacks, Spacers, and TikTok existed).

You’ll see that working with Auto-Layout not only teaches us how to make rectangles show up on a screen, but also leaves us a few life lessons along the way. Get comfortable, play some lo-fi, and let’s embark on this introspective journey together 🧘🏽‍♂️ —

Why Auto-Layout? 😶

To fully understand what Auto-Layout is, we first have to look at the problem it’s trying to solve.

Frames 🖼

Originally, views give superviews context by setting an explicit position and size (a frame) in order to place themselves on-screen.

The superview takes this context and tells its coordinate system exactly where and how large it should display.

What we’re going to do — Using an iPhone 8, we want to display two identically sized squares next to each other and fill to screen width. We’re going to apply 20 points of padding to the left and right side of the squares, as well as 20 points in between:

Justifications:
An iPhone 8’s point dimensions are 375 x 667
view1 x-origin = 20 (Full left padding)view2 x-origin = 375/2 + 10 = 197.5 (Centered with half-padding)Both width/height = 375/2 - 20 - 10 = 157
(Half the screen width without left and right side paddings)
Both y-origins = 250 (Arbitrary for our requirements)

After applying the most math I’ve done since refinancing my student loans, we find exactly what origin and dimension values we need in order to make the context for our superview.

We’ll then create a CGRect frame with these values to instantiate our UIViews with. Now when we add these views to any arbitrary superview, that superview will use this frame to lay it out in its own coordinate system.

It doesn’t matter at which point in the code we add these views as subviews, since it’s the views themselves that holds the information of their frames.

We can technically add these subviews until the last configuration lines and it’ll still display correctly!

Now that we have our views, let’s run the app and see how it goes —

Figure 1.A
iPhone 8 (375 x 667)

Looks good 👍🏽

Since we were specific in where and how large to place view1 and view2, we see that the superview sets them as expected.

If there’s no anticipated changes in the UI, then each subview’s frame will always be relevant to the context of its superview’s coordinate system.

But what happens when the superview’s frame changes, or even of one of its subviews? Let’s run the app again with the same values, but this time with an iPhone 11 Max

iPhone 11 Pro Max (414 x 896)

Oh no 🤭

Since the superview’s frame changed from 375x667 to 414x896, our original calculations based on the iPhone 8 dimensions failed to scale.

Sure we can make a constant for the width value to something like the UIScreen’s width, but what if the culprit of the change is the subview’s content? 🙊

We won’t know how to accommodate the subview’s frame from the super’s perspective without some serious effort.

And as self-proclaimed “good programmers”, we hate effort. Specially the serious kind.

Accommodating phone sizes, anticipating dynamic view content, or even adjusting for landscape mode can be very annoying when recursively calculating the frames in your view hierarchy.

What is Auto-Layout exactly? 💀

That’s why Auto-Layout exists. By using constraints to represent the relationship between views, Auto-Layout generates these frames for you. It calculates the size and position on your behalf as long as the rules you set (the constraints) result in one sound outcome.

Constraints and Attributes ⛓

In short, constraints are the relationship between one or two participating view’s edges, sizes, or centers, usually with some sort of modification like an offset or multiple. They’re the building blocks of Auto-Layout.

To represent the ‘something’ that pins views together, we sort them into distinct categories. We can just call the ‘somethings’ attributes.

Attribute Categories —Horizontal: Left, Right, Leading, Trailing, and Center-XVertical: Top, Bottom, and Center-YSize: Height and Width

Note: Leading and Trailing are functionally similar to Left and Right, but can change depending on device locale. A leading anchor is left to the view in USA locales, but right in Japanese locales. Vice-versa for trailing. Use them instead of left and right constraints if you need to localize your app for these locales. 🔀

Now that we have our attributes, let’s take a look at applying them! I like to think of the anatomy of a constraint in roughly three parts 🍡 —

Participants: The one or two participating attribute(s)Inequalities: The type of relationship between participant(s)Modification: Some sort of optionally applied defined change

You can also only set constraints between ones in each category, meaning you can’t pin a view’s top constraint to another view’s left and so on.

Let’s take a look at how a constraint is made with a basic NSLayoutConstraint. Referencing our squares example, we’re going to pin the left side of the first square to the left side of the superview:

Note: In order to let Auto-Layout know you wish to use this constraint, you need to set the constraint’s isActive property to true.

  • The participants 👬 in this constraint are view1 and its superview (the view controller’s view).
  • The inequality ⚖️ is indicated by the relatedBy parameter, and in this case set to equal, but can also be set to lessThanOrEqual or greaterThanOrEqual. This means that if a constraint needs to grow or shrink due to some frame change, you’re giving it permission to do so.
  • The modifications 🏎 in this constraint is an offset of 20, meaning view1’s left is going to be equal to view’s left, but shifted to the right by 20 points. The multiplier is also a modification, but since we’re not dealing with sizes, it doesn’t make sense to change it.

There’s one cardinal rule when it comes to working with constraints. They all have to play nice with each other, meaning constraints generally need to implicitly convey the position and size of your view, instead of explicitly like our frames did.

The superview still needs a size and position for their subviews, so constraints need to provide that information. The only difference is that information is given through relationships, not through hard numbers. Because why do math when you can do not-math? 💩

The game plan — Our constraints for our original example will look like this:

Left square:The left attribute must equal the superview’s left + 20 pointsThe top attribute must equal the superview’s top + 250 pointsThe width must equal the super's width divided by 2 - 30 pointsThe height attribute must equal its own width attributeRight square:The left attribute must equal the left square’s right + 20 pointsThe height and width attribute must equal the left’sThe centerY attribute must equal the left square’s centerY attribute

Note: Since the coordinate system for any view originates from the top-left corner (where origin = CGPoint(x: 0, y: 0)), all positive offsets will shift a constraint in the down/right direction, while negative offsets shift in the up/left direction.

Let’s break this down per object —

Left Square:

  • We define the size by indicating the width as equal to the super’s width along with an offset of -20 points applied, and the height equal to its own width (or equal to the same constraint our width depends on).
  • We define the position by indicating it must equal to the super’s left attribute (x-axis) with a 20 point positive offset to shift it to the right, and equal to the super’s top attribute (y-axis) with a 250 point positive offset to shift it down the screen.

Right Square:

  • We can imply where the position is because we know how far left (y-axis) it needs to be by making its left attribute equal to the other square’s right attribute (with padding), as well as how for down (x-axis) by making its y-center attribute equal to the left square’s y-center.
  • We know the size of it by setting its width and height constraint equal to the left’s size.

No rules are being broken by these relationships, but let’s say we leave out the second requirement for the right square. Now Auto-Layout doesn’t know how large the right square needs to be anymore.

It doesn’t play nice. This is going to cause some issues during run-time, since in most cases the compiler won’t throw a fit over some missing constraints. We give the position of the right square, but not the size. Constraints need to fulfill both, since it’s going to be rendered down to frames later on! 😮

How do we implement Auto-Layout? 🤭

There’s plenty of tools when it comes to Auto-Layout. Through trial, error, and deprecation, we find a set of practical approaches for modern day layout development —

  • We can do it programmatically, with NSLayoutAnchor (the successor of NSLayoutConstraint after iOS 8*)
  • We can do it visually, with Interface Builder (IB) and Storyboards
  • Or we can even take a non-native approach with third-party kits and DSLs like SnapKit, which is built on top of native constraints

*Since we’re currently at iOS 13, let’s assume we’ll just need to run iOS 10+

Picking the right Auto-Layout tool can be tricky. We need to really reflect on why Apple and friends felt the need to give us so many (it could be for no reason honestly). But there probably is a reason! So which ones do we choose? Which ones Josh?? What was the reason?? WHAT WAS THE REASON??

NSLayoutAnchors 🧘🏽‍♂️

You’ve got to find yourself first. Everything else’ll follow — Charles De Lint

You know how I mentioned that constraints need to have participants in the same attribute category? Before NSLayoutAnchors, its predecessor NSLayoutConstraint didn’t have any checks on the constraint category of its attribute parameters.

This means that you can compile and run a constraint that pins a view’s right to another view’s top (which crashes your app)! 😬

To fix this, and to provide a more concise syntax, NSLayoutAnchors were given the power of self actualization. They were built with type-safety in mind and only sets constraints between attributes of the same category. 🤝

The anatomy of an NSLayoutAnchor can be skimmed down to —

<someView>.<someAnchor>.constraint(<someInequality>: <nextAnchor>)

Note: The someAnchor param and nextAnchor must be of the same category, or the compiler will throw up before you get the chance to run and crash —

The parameters are also a subset of the full parameter list in NSLayoutConstraint, which means less code and less ambiguity.

You ask for what you just need, and if you need something extra, they have a full list of convenience inits to compensate for fewer parameters.

So back to the squares, but this time with our new friend NSLayoutAnchor ⚓️

There’s one thing we need to do before setting up our constraints. Since we don’t want autoresizing masks to interference with Auto-Layout in creating constraints, we want to set the participating views’ translatesAutoresizingMaskIntoConstraints values to false.

view1.translatesAutoresizingMaskIntoConstraints = false

Let’s highlight some of the NSLayoutAnchor functionality above —

Earlier we mentioned how parameters are in a need to know basis, so we see below that a constant parameter can be appended to the list if you need to add optional padding (which also compiles and runs without it)!

view1.topAnchor.constraint(equalTo: view.topAnchor, 
constant: 250).isActive = true

Below, leadingAnchor and trailingAnchor are of the same category (horizontal), so the compiler accepts this as a valid constraint —

view2.leadingAnchor.constraint(equalTo: view1.trailingAnchor, 
constant: 20).isActive = true

Also along with constants, multipliers can be inserted in the parameter list in situations where you need a scaled relationship between two size attributes.

view1.widthAnchor.constraint(equalTo: view.widthAnchor, 
multiplier: 0.5,
constant: -30).isActive = true

We run the app and see that it displays perfectly on an iPhone 11 Pro Max! 🎉

Storyboards & IB 🗺

“If you can dream it, you can do it.” — Walt Disney

There’s a certain sense of gratification when you see your final product after spending so much time fine-tuning it. As someone whose attention span has been permanently ruined by modern media, I could care less.

I want that sweet-sweet instant gratification for what I’m doing right here, right now, yesterday, the day before, last year, before I was born —

That’s the cool thing about Storyboards: You see what you get. There’s no frills, you click and drag constraints, and Interface Builder (the editor that Storyboard runs on) updates the UI in real time!

Figure 1.A

The view controller for this example has our two squares as subviews to its content view via IB.

Constraints between views can be created by hovering over one view and clicking control and dragging to a second view (or itself).

Release it and a tooltip appears where you can select the attribute(s) for the constraint created between these views, setting our participants.

Once you create your constraint, you can also edit them in the interface on the right hand side in order to fine tune it. You’re given the option of changing your items (participants), changing your relation (inequalities), or changing your constants/multipliers (modifications), all within one convenient toolbox!

Figure 1.B

Below is the same line of code in Figure 1.A that we implemented in IB with just one click and drag:

view1.leadingAnchor.constraint(equalTo: view.leadingAnchor,
constant:
20).isActive = true

If you need more control of the constraint outside of IB, we can even create an IBoutlet if we need to update any properties (ie. isActive) during runtime in code —

We don’t even need to set the participating views’ translatesAutoresizingMaskIntoConstraints property since IB takes care of that for us.

Let’s recreate all the constraints we have for our NSLayoutAnchor example in Storyboard. We see that the constraints we created via drag and drop appear on the right hand side of the editor under the constraints tab for that view—

The interface here is super helpful with its line guides. We see how each constraint is applied to the view, even with the padding indicated. ✨

This is a very powerful visual tool that we don’t get through programmatic approaches. If we have any conflicting constraints, IB lets us know right away by highlighting in red any breaking constraints.

Storyboards have their own set of benefits over a programmatic approach.

  • Navigation is explicitly indicated, making your view controller flows have visual representations.
  • You will write significantly less code, making this method super appealing for codebases that suffer from view layers that require more UI logic.
  • Because of that, it’s a great fit for design patterns that preach about single responsibility. (MVVM, VIPER, MVLMNOP, QRS, TUV)

Note: Storyboards get transcribed to XML, and in some instances this might not play well with version control. This can definitely be mitigated by practices like using multiple storyboards, or not committing the automatic updates IB applies when you open it.

We run the app and see (to no surprise) that the UI has rendered exactly how IB displayed —

SnapKit & Third-Parties 👉🏽👈🏽

There is no need to suffer silently and there is no shame in seeking help. — Catherine Zeta-Jones

I love SnapKit so much. If there weren’t any significant pros and cons between choosing a UI tool for a project, I’d go with SnapKit more times than I’d like to admit. 💩

Seems a bit excessive (and irresponsible), but the syntax is just so nice. Although I know.. No matter how nice a toolkit is, or how convenient it is to write layout code in fewer lines, SnapKit (and friends) are still third party.

Bringing in a dependency to your app can be a big decision. If you choose one that ends up being unsupported in newer versions of iOS, it can tie down your app to the previously supported version until you find a solution. This forces you to either remove that dependency and implement a native fix, or fork the dependency and fix it yourself. 🤡

With first party solutions like NSLayoutAnchor readily available for recent iOS versions, it’s getting hard to justify going third party. Sure you get more out of the box with something like SnapKit, but I found that most of what you need is already within UIKit. (Nick if you’re reading this, I regret nothing)

And if there’s anything extra that’s missing in your app level toolkit, you can always write an extension or app level util to accommodate.

So with that disclaimer out of the way, we can get into why SnapKit is the best solution for any product, any use case, any iOS version in the past, present, and all future timelines that involves an iPhone.

The syntax for creating a constraint can be divided into the following —

<someView>.snp.makeConstraints <Make Closure>

Each UIView will have a property called snp which is provided by SnapKit. You can either call makeConstraints, updateConstraints, or remakeConstraints.

As the name suggests, SnapKit not only lets you create constraints initially, but also lets you update all/remake a subset of the constraints indicated inside your Make Closure:

Make Closure = ((MakeObject) -> Void)

After calling your intended constraint method, you’ll have to create your constraints inside the trailing closure provided.

Note: All constraints inside of it will activate automatically, meaning you’ll have to create some sort of reference to your SnapKit constraint (the Constraint object) in order to call deactivate() (which is their way of setting the isActive property to false).

Constraints in SnapKit follow a minimalist syntax —

$0.<Attribute>.<Inequality>(<Attribute>).<Optional Modification>

You can set the participants, inequality, and optional modifications all in one short line. 😮

Below, we recreate our squares example one last time —

There’s a lot of ~🆒~ things going on here.

First, you can chain constraints together so you won’t have to write multiple lines. This is super useful if you have a certain view’s attributes that have some sort of even relationship to another view’s single attribute —

make.width.height.equalTo(view.snp.width).dividedBy(2).offset(-30)

Here, both view1’s height and width attributes are fractionally related to the superview’s width by the same divisor and offset, so we can apply the appropriate constraint to both of them by chaining them together!

If you ever want to pin a view’s edges to the superview’s, it’d be as simple as calling —

make.leading.trailing.top.bottom.equalToSuperview()

You can even call conveniences like edges to indicate you want to create constraints for all surrounding x and y-axis attributes for a view —

make.edges.equalToSuperview()

You can apply modifications like an offset, inset, or multiple by appending a method call at the end of your constraint declaration, which applies it to any attribute indicated on the left hand side (even chained ones) —

make.edges.equaltoSuperview().inset(
UIEdgeInsets(top: 5, left: 20, bottom: 5, right: 20)
)

And it goes without saying that translatesAutoresizingMaskIntoConstraints are already accounted for. 😌

This is just a small subset of the functionality you get out of the box, and if the use case fits your app for those conveniences, I’d recommend giving it a try to see how it can clean up your UI!

What I love about SnapKit specifically is that you can really get creative with your components. Custom programmatic UI can be a breeze to wire up, and the number of lines you’ll have to debug are a fraction of what an NSLayoutConstraint implementation would take.

We run the app and see the last example for our even squares 😢 —

Will you actually need half of the functionality you get from importing this? Probably not.

But why do people buy 1TB iPad Pros just to use Procreate? Why do we opt for new 13" Pros when our 15" MacBooks from a few years ago runs perfectly fine? Why do we get Hulu+ when we know we’re only going to watch maybe 3 channels from it?

Because it’s cool.

Because it’s fun to have more things.

Because no one told me any better when I first started out. 💩

Making decisions 😨

I love programmatic layout, whether first party or not. I love how concise it is with the right extensions. I love how you can line break to debug and don’t have to visually diagnose the storyboard when constraints aren’t pinning correctly and so on.

  • “It’s easier to componentize UI.”
  • “It makes version control way more streamline.”
  • “You get more control and customization out of your UI.”

And while there are grounds for these arguments, after getting the opportunity to dig a little deeper and explore IB, I found there are also plenty of benefits using the IB approach outside of bare code.

  • The volume of your interface code is seriously cut down.
  • You can leverage XCode’s powerful built in auto layout functionality within Storyboard.
  • You can literally see what you’re doing to your UI before running the app so runtime errors are significantly mitigated.

The features I’m writing now, which I assumed would take a lot of wrestling in IB, have been pretty stress free to implement. I may still have a personal bias for programmatic layout, but for what it’s worth, even the Apple Documentation states:

“Whenever possible, use Interface Builder to set your constraints. Interface Builder provides a wide range of tools to visualize, edit, manage, and debug your constraints.” — Tim Cook (probably)

But their opinion doesn’t matter either. 😬

At the end of the day, the benefits of code-only or storyboard solutions are all moot when you don’t consider the impact to your codebase, team, or process.

So to answer the question… It depends.

(I know, but you saw it coming a mile away) 🙃

As a closing note, for you and me, we shouldn’t dismiss an approach due to predisposed notions without exploring how it’ll fit in our day-to-day, because there’s only one wrong way when it comes to implementing interfaces in Swift…

…frame-based layouts 🥴

Thanks for reading! I hope y’all are staying safe at this time. If you have any questions, don’t like the article, or just want to say hi — feel free to leave a comment! If you want to dig around, here’s a link to the code:

--

--

Responses (1)