SwiftUI Layout System | kean.blog

 
Everything about SwiftUI is new. And the layout system is no exception. SwiftUI no longer uses Auto Layout, gone all of the cruft introduced over the years. SwiftUI has a completely new layout system designed from the ground up to make it easy to write adaptive cross-platform apps.
I have always been fascinated by the layout systems. I built an open-source UIStackView replacement, designed a convenience API on top of Auto Layout (granted, many people did). I also have experience working with the layout systems on the web, including Flexbox. I can’t be more excited to dig deep into the SwiftUI layout system to see what it has to offer.

Layout Basics

Let’s start with the most basic “Hello World” example.
This is the code that gets generated when you select File / New File... / SwiftUI View in Xcode.
notion image
The moment you open the preview, you are already experiencing the layout system. The blue box in the preview editor indicates the bounds of the ContentView on screen. The bounds of the view are the same as its body, the text, which is at the bottom of the view hierarchy1. And finally, the root view which in this case has the dimensions of the device minus the safe area insets.
The top layer of any custom view, like ContentView, is layout neutral. Its bounds are defined by the bounds of its body, in this case, Text. For the purposes of layout, you can treat the custom ContentView and Text as the same view. Now how did SwiftUI establish the bounds of the ContentView and why did it it position it in the center of the root view? To understand this, we need to understand how SwiftUI layout system works.

Layout Process

There are three steps in SwiftUI layout process.

1. Parent Proposes Size for Child

First, the root view offers the text a proposed size – in this case, the entire safe area of the screen, represented by an orange rectangle.

2. Child Chooses its Size

Text only requires that much size to draw its content. The parent has to respect the child's choice. It doesn't stretch or compress the child.

3. Parent Places Child in Parent’s Coordinate Space

And now the root view has to put the child somewhere, so it puts in right in the middle.
notion image
This is it. This is a simple model, but every layout in SwiftUI is calculated this way. This is a major departure from Auto Layout in many important ways.
First, it is in some ways closer to simple frame-based layout where the child had no affect on the parent’s frame. This wasn’t the case with Auto Layout where constraints work in both directions: in some cases, a parent would determine the size of a child, but sometimes it was the other way around. This was a major source of complexity in Auto Layout and it is gone now.
Second, as you might have noticed, we haven’t explicitly said anything about the layout, but there were no “Ambiguous Layout” warnings. Unlike Auto Layout, SwiftUI always produces a valid layout. There is no such thing as an ambiguous or an unsatisfiable layout2. The system does its best to always produce the best result and give you the control when needed.
Now that we’ve looked at the most basic example and have an idea of how SwiftUI layout process works, let’s see what instruments does it offer. In Auto Layout, all the APIs were built on top of the same technology - constraints. This isn’t the case with SwiftUI in which everything: Stacks, Frames, Paddings, etc – is its own thing. To understand the layout system means understanding all of these instruments. Let’s start with the most basic one - frames.

Frame

First, forget everything you know about frames in UIKit or AppKit. Those have nothing to do with frame(width:height:alignment:) and other related methods in SwiftUI.
Let’s take a 60x60 image and display it using SwiftUI’s Image. Look what happens if I set the frame to 80x80.
notion image
The image has not changed its size. Why is that? A frame in SwiftUI is not a constraint. Neither it is the current frame or bounds of the view. Frame in SwiftUI is just another view which you can think of like a picture frame.
By calling Image("swiftui").frame(width: 80, height: 80), SwiftUI creates a new invisible container view with the specified size and positions the image view inside it. The layout process then performs the same steps as we just described previously. The new container view proposes its child, Image, the size 80x80. Image view responds that it is only this big – 60x60, but thank you anyway. The Frame needs to put the image somewhere, so it puts the image in the center – it uses .center alignment by default.
The alignment parameter specifies this view’s alignment within the frame. The default one is .center, but you can select any of the other available ones:
notion image
In SwiftUI, unless you mark an image as resizable, either in the asset catalog or in code, it’s fixed sized. If marked resizable, frame now directly affects the size of the image view:
notion image
All of the parameters of the frame(width:height:alignment:) method are optional. If you only specify one of the dimensions, the resulting view assumes this view’s sizing behavior in the other dimension.
Finally, let’s see what happens when the frame is smaller than the size of the content.
notion image
Like any other view, the child ultimately chooses its own size. It is important to understand this property of the SwiftUI layout system.
We used alignment to position the child inside the frame. Other instruments can be used to position the child in its parent’s coordinate space, like position(x:y:) and offset(x:y:). And frame(width:height:alignment:) is not the only way to specify a frame for the view. There is another variation that allows you to specify minimum, maximum and ideal width and/or height which I not cover in this post.

Stacks

When creating a SwiftUI view, you describe its content in the view’s body property. However, the body property only returns a single view. You can combine3 and embed multiple views in stacks4.
  • HStack group views together horizontally
notion image
Stacks are the primary layout instrument in SwiftUI. The vast majority of layouts can be implemented using stacks. Stacks might seem almost too simple, and they are5. But don’t underestimate them. You are going to use stacks a lot in SwiftUI. Understanding how they work is probably more important than anything.

Stack Layout Process

There are three simple steps in the stack layout process.
The first and the last steps probably don’t require any explanation. Step two, on the other hand, might be hard to wrap your head around. I think the best way to understand it is by going through a few examples.
Let’s start with a simple example with two image views. The size of each image is 80x80 points, which is non-negotiable (use resizable to allow an image to resize). Regardless of what size the stack proposes to any of the images on step two, the image always returns 80x80. The size of the stack itself, with spacing 10, is always going to be 170x80, regardless of the size of its parent.
notion image
notion image
This first example again shows that in SwiftUI the child ultimately chooses its size. In that regard, SwiftUI layout feels much lighter and more manageable than Auto Layout. There are no layout errors, the stack doesn’t arbitrarily resize any of the images. SwiftUI always produces a well-defined result.
Let’s now look at another example. This time, let’s throw some more flexible views into the mix - Text views. The scenario where everything fits is not particularly interesting. The question is what happens if it doesn’t?
notion image
notion image
In the first example (top screenshot) the width of the preview is 230. The spacing is 10, so the initial unallocated space is 210 (230 - 10 * 2). So, now the stack needs to calculate the size of its children.
  1. The stack splits the space into three equal parts, each of the width 70.
  1. It then proposes this size to the least flexible child. In our case, it’s Image. Its size is 80x80. So that view is out of the picture. The order is irrelevant. If you move the image to another position, the layout stays the same.
  1. The stack subtracts the width of the image from the remaining space, so the remaining space is 130 (210 - 80).
  1. The stack again splits the space into two equal parts, each of the width 65.
  1. It proposes the size 65x120 to the first text view. The text responds that, yeah, the content won’t fit, but it can manage to display at least a portion of it. The same happens with the second text view. So even though the text views had a different amount of text, they both got the same width.
In the second example (bottom) we give the stack a little bit more space. So the second text now fits. But because the stack always splits the unallocated space into equal parts, the second text view occupied almost all of the available space.
notion image

Environment

Ok, so frames and stacks are great. But what makes SwiftUI also stand out in terms of supporting adaptive cross-platform apps is the environment.
When you add padding to a view, SwiftUI automatically chooses an amount of padding that’s appropriate to the platform, dynamic type size, and environment. SwiftUI also automatically sets appropriate spacings between the views in a stack. SwiftUI updates a safe area according to the device. You get the idea.
When you don’t pass any parameters, you get adaptive behavior in the same way that SwiftUI adaptively styles a picker or a button depending on the context it’s in. And if you to customize any of these parameters, you can do that too.
SwiftUI also makes it easy to react to the changes to the environment. Every view in SwiftUI gets access to the system environment settings like content size category, device size classes, layout direction, and more. So, for example, if you want to set custom spacings for each size category, you can do that easily with SwiftUI.
By using @Environment property wrapper, you can read the environment values and subscribe to their changes. Technically, the environment is not part of the layout system, but I think it was worth mentioning it.

Final Thoughts

With Auto Layout, Apple took a solution – a layout engine Cassowary, and tried to make it fit the problem – building adaptive user interfaces. It was powerful, but it was lacking in many important areas. It had performance issues, it was complex, debugging it was hard. Apple tried to make it better by introducing more and more Auto Layout APIs over the years: anchors, stack view, safe area. But they never fixed the core problems with the technology.
In Auto Layout one rogue constraint could lead to completely unpredictable results far from the view where it was defined. SwiftUI, on the other hand, is simple and predictable. It should be always possible to understand at a glance why the layout system produces certain results.
You can feel that SwiftUI was created with a completely different mindset than Auto Layout. It is not an academic exercise to efficiently solve systems of linear equalities and inequalities. It is a pragmatic tool designed to solve the real problems that app developers face when creating adaptive cross-platform apps for Apple platforms. And it solves them in a beautiful way by providing a set of small and simple tools which are easy to combine – the Unix way6.
Swift dominance didn’t come from the server, it just might from the UI.