Blog

Latest Post

SwiftUI Layout Explained: Free to watch!

🌲 As a little Christmas present to the community, we're making our entire SwiftUI Layout Explained video series free to watch until the end of the year. 🌲

A central piece of every UI framework is its layout system, and SwiftUI is no exception. Without a good understanding of the layout system, building user interfaces quickly becomes frustrating.

We did a lot of research for our book, Thinking in SwiftUI , but even so we kept encountering layout behavior that we couldn't really make sense of.

We decided to go one step further, and reimplement SwiftUI's layout system, along with the layout behavior of many built-in views. This forced us to think hard about the algorithms, and helped us understand SwiftUI's implementation by comparing it to our own.

All of this is documented in our latest Swift Talk collection: SwiftUI Layout Explained . With eleven episodes, five hours of live-coding and discussion, a hand-written transcript and sample code, there's plenty to enjoy over the winter break.

Happy learning and happy holidays! 🍷

Best from Berlin,

Chris & Florian

Previous Posts

Book Update: Thinking in SwiftUI

Over the last few months, we have been busy updating our book, Thinking in SwiftUI . Today, we're glad to release it as a free update, improved and expanded for the latest version of SwiftUI.

The new version refines existing explanations and adds sections for new features. Writing the original book, we decided to focus on the essence of SwiftUI, and luckily very little has changed: if you have read the first edition, you will be well-prepared for any upcoming changes.

The most important additions include:

  • New sections on function builders — they now support switch statements, if let , and have better diagnostics.

  • Several sections on matchedGeometryEffect — one in the Advanced Layout chapter, and one in the Animations chapter.

  • A section on grid views — once you go beyond the basics, they can have very unexpected layout behavior!

We learned a lot more about SwiftUI's layout system while working on SwiftUI Layout Explained , our latest Swift Talk collection. The series helped us improve our explanations of both stack views and layout priorities. Reimplementing the most important parts of SwiftUI's layout system, and heavily testing the results, gave us a unique insight into how things actually work.

Finally, we have added an expanded section about data flow in views, describing the differences between displaying values and objects, as well as discussing ownership in depth. This includes an explanation of how to choose between @StateObject , @ObservedObject , @State and @Binding .

This update is free for everyone who bought the Ebook directly (through our sales partner, Gumroad), either standalone or as part of a bundle.

Happy holidays! 🍷

Florian and Chris

Happy Black Friday! 🛍

We're delighted to announce a 30% discount on all our eBooks and Swift Talk subscriptions until Monday!

This year, all our bundles are included, with a further 30% off the regular bundle discount! For example, The Complete Collection , including all six books and videos for Advanced Swift, App Architecture and Thinking in SwiftUI, is now only $209, our largest saving at $90 — if you could stack eBooks, they'd look great under the Christmas tree. 🎄📚

To apply the discount, use the promo code thanks2020 when buying a book from our website.

In a rare special, Swift Talk is also 30% off . We publish a new live-coding episode every week, discussing specific real world problems and developing solutions live, while you watch. Unlike traditional tutorials, Swift Talk is conversational in style, which makes it easier to follow our thought processes and helps you understand why we choose particular techniques.

By the way: our book Thinking in SwiftUI will be updated very soon (we're finalizing the changes) and will be a free update.

Enjoy! 😊

SwiftUI’s Grid Views

In our current Swift Talk series , we have been re-implenting parts of SwiftUI's layout system to gain a deeper understanding of how it works. Last week we examined SwiftUI's grid views and re-implemented their layout algorithm. Some parts of the behavior really surprised us.

A few days ago we tweeted a series of layout quizzes for SwiftUI's LazyVGrid to highlight some of the less obvious behaviors. In this post we'll take a look at all three quiz questions and explain why the grid lays out its contents in the way it does.

Interestingly, we were not the only ones struggling to understand the behavior of grids: none of the most popular quiz answers were correct!

#1: Fixed and Adaptive Columns

The first example defines a grid with a fixed and an adaptive column. The grid has a fixed width of 200 points:

								LazyVGrid(columns: [
    GridItem(.fixed(70)),
    GridItem(.adaptive(minimum: 40))
]) {
    /* ... */
}
.frame(width: 200)
.border(Color.white)

							

The three solutions we asked you to choose from:

Solution A is correct.

For grids, fixed size columns are always rendered exactly at the specified width, no matter how much space is available. Therefore, the first column renders as exactly 70 points wide.

The grid subtracts the fixed column widths and the spacing between columns (which is the default spacing of 8 points) from the proposed width (which is 200 points in this example), leaving us with a remaining width of 122 points. This width is then distributed to the remaining columns. In this case there's only one column left, an adaptive column, so it takes up the remaining width of 122 points.

Adaptive columns are special: SwiftUI tries to fit as many grid items as possible into an adaptive column; it divides the column width by the minimum width, taking spacing into account.

In our example above, we have a default spacing of 8 points, and we can fit two columns (40 + 8 + 40) into the 122 points. Trying to fit three columns (40 + 8 + 40 + 8 + 40) fails. The adaptive column has an effective width of 122 minus 8, giving 114 points to distribute. Dividing 114 points by the number of columns gives us two items that are 57 wide.

#2: Flexible and Adaptive Columns

The second quiz has a grid with a flexible and an adaptive column:

								LazyVGrid(columns: [
    GridItem(.flexible(minimum: 140)),
    GridItem(.adaptive(minimum: 70))
], content: {
    /* ... */
})
.frame(width: 200)
.border(Color.white)

							

The three potential solutions:

Solution C is correct.

Unlike the first example, this grid doesn't have any fixed columns, so the grid starts out with an available width of 200 points minus 8 points of default spacing between the two columns, which gives us 192 points.

Then the grid loops over the remaining (non-fixed) columns in order, and calculates each column width as remaining width divided by the number of remaining columns, clamped by the minimum and maximum constraints on the columns.

In this example, the width of the first column is calculated as 192 points of remaining width divided by 2 remaining columns, which equals 96 points. Since we specified a minimum width of 140 points for the first, flexible column, 96 points gets clamped to the range between 140 and infinity. The column becomes its minimum width of 140 points, and the remaining width is now 192 minus 140, giving 52 points.

The width of the second column is now calculated as 52 points remaining width divided by 1 remaining column, which equals 52 points. We might expect this result to be clamped to the adaptive column's minimum width of 70 points, but the minimum property of an adaptive column is only used to compute the number of items inside that column. The adaptive column thus becomes 52 points wide.

The item in the adaptive column is being rendered with a width of 52 points as well, although we've specified a minimum of 70 points. If there's less space available than the minimum item width, the minimum width is ignored and the item gets rendered at whatever width is left over.

#3: Multiple Flexible Columns

The third quiz has a grid with two flexible columns:

								LazyVGrid(columns: [
    GridItem(.flexible(minimum: 50)),
    GridItem(.flexible(minimum: 120))
], content: {
    /* ... */
})
.frame(width: 200)
.border(Color.white)

							

The three potential solutions:

Solution A is correct.

This seemingly simple layout shows perhaps the most confusing behavior of the three examples in this post. Not only does the grid render out of bounds — although the columns' minimums would happily fit into the available width of 200 points — it also renders out of center of its enclosing 200-points-wide frame.

Let's go through the steps of the grid's layout algorithm and see what's happening. We start again with a remaining width of 200 points minus 8 points of default spacing, which gives us 192 points. For the first column, we calculate the width as 192 divided by 2 remaining columns, which equals 96 points. Since the first column has a minimum width of 50 points, the width of 96 points isn't affected by the clamping, so the remaining width stands at 96 points. The second column becomes 96 points clamped to its minimum of 120 points, i.e. 120 points wide.

However, that's not what we see in the rendering of this grid: the first column renders 108 points wide, the second one renders 120 points wide, whereas we calculated 96 points and 120 points.

To understand this part we have to remember that SwiftUI first calculates the frames of all views, before it renders the views in a second pass. With our calculation above, the overall width of the grid is calculated as 96 + 8 + 120 = 224 points. The fixed frame with a width of 200 points around the grid then centers the grid, shifting it (224-200)/2 = 12 points to the left.

When it's time for the grid to render itself, it starts out with the width determined in the layout pass, which is 224 points, but to actually render it calculates the column widths again based on the width of 224 points!

In this regard, grids differ significantly from stacks: stacks will remember the sizes of their children between layout and rendering, and therefore avoid this unexpected behavior, whereas grids don't seem to do that.

Let's go through the column width calculations once again, starting out with a remaining width of 224 points minus 8 points spacing, or 216 points. The first column becomes 216 points divided by 2 remaining columns, equalling 108 points. The remaining width is now 216 minus 108, giving us 108 points. The second column becomes 108 points clamped to its miminum of 120 points.

Et voilà! We've arrived at the correct column widths of 108 and 120 points.

Since the frame around the grid has calculated the grid's origin based on the original width of 224 points, but the grid now renders itself with an overall width of 108+8+120 = 236 points, the grid appears out of center by 6 points.

Conclusion

In summary, these are the steps the grid's layout algorithm takes:

  1. Start out with the proposed width as the remaining width.

  2. Subtract the width of all fixed width columns, as well as the spacing between columns.

  3. Iterate over the remaining columns in order and

    • calculate each column's width as remaining width divided by the number of remaining columns, clamped to the column's minimum and maximum.

    • subtract the column's width from the remaining width.

Keep in mind that this algorithm runs once during layout, and then again during rendering. During layout the algorithm starts with the proposed width of the parent view, whereas during rendering it starts with the calculated overall width from the initial layout pass.

This behavior can be quite unintuitive, but we hope that this post will help you understand that behaviour better, and achieve the results you aim for.

We'll be continuing the SwiftUI Layout Explained series through December. Each episode re-implements an aspect of the layout system, and with six episodes already released there's plenty to learn!

To support us, and access our entire catalogue, subscribe here .

How an Hstack Lays out Its Children

For the most part SwiftUI's layout system is intuitive to use, letting you achieve what you want with a little bit of experimentation. However, sometimes you encounter behaviors that are hard to reason about without a deeper understanding of the underlying implementation.

HStack s are a good example of this: much of the time they work as you'd expect, but sometimes their behavior can be puzzling. A good way to reduce this puzzlement is by attempting to replicate the behaviours yourself. Through a process of ever-closer approximation, we can better understand the underlying mechanics.

In this article we'll talk about HStack , but the same logic applies to VStack , except that the axes are flipped.

Let's start with a simple example that behaves just as you'd expect:

								HStack(spacing: 0) {
    Rectangle().fill(Color.red)
    Rectangle().fill(Color.green)
    Rectangle().fill(Color.blue)
}
.frame(width: 300, height: 100)

							

Each view becomes 100 points wide. The black border visualizes the bounds of the stack.

To understand SwiftUI's layout behavior by experimentation, it's helpful to visualize the view dimensions. We can create a simple helper that overlays the width of a view:

								extension View {
    func measure() -> some View {
        overlay(GeometryReader { proxy in
            Text("\(Int(proxy.size.width))")
        })
    }
}

							

By adding a .measure() to each of the three subviews, we get the following image:

So far, so good. Where things become interesting is when we add views that have a different flexibility.

For example, consider the following:

								HStack(spacing: 0) {
    Rectangle().fill(Color.red).frame(maxWidth: 100).measure()
    Rectangle().fill(Color.green).frame(minWidth: 100).measure()
}.frame(width: 150, height: 100)

							

How wide do the individual views become?

If you're used to constraint-based layout, you might reason like this: there are 150 points of available space and the green child needs to be at least 100 wide. Therefore, assigning the red child a width of 50 solves all the constraints. However, that's not at all how HStack s lay out their content.

As we can see below, the red rectangle becomes 75 wide, and the green rectangle becomes 100 wide. The HStack even draws out of bounds of its enclosing frame!

Why is this happening? To understand this behavior we need to revisit the WWDC session Building Custom Views with SwiftUI , and look at the algorithm they explain.

Because the stack gets proposed a width of 150, it first proposes half of that (75) to the red child. The rectangle's frame accepts that width, as does the rectangle, and the entire subview becomes 75 points wide.

Then, the stack proposes the remaining width (75) to the green child. Because it has a minimum width of 100, the view becomes 100 points wide. The HStack itself becomes 75 + 100 = 175 points wide.

If we swap the two subviews around, we get the exact same result, only drawn in a different order. This is because the stack orders its children by how flexible they are, and then processes them from least to most flexible child.

However, the meaning of "least flexible" is not defined, in either the WWDC session, or in the documentation. To figure out the order of flexibility, we spent much of our time carefully experimenting, and we came up with the following:

From least to most flexible

  • A fixed size view

  • A view with both a minimum width and a maximum width

  • A view with just a maximum width

  • A view with just a minimum width

For views with the same flexibility according to the list above, we had to sort again based on their minimum or maximum widths. We tested this exhaustively and it turned out to be a good intuition, but not quite the whole truth.

For example, consider the following hierarchy:

								HStack(spacing: 0) {
    Rectangle().fill(Color.red).frame(maxWidth: 100).measure()
    Rectangle().fill(Color.green).frame(minWidth: 90, maxWidth: 200).measure()
}
.frame(width: 150, height: 100)

							

According to our sorting, the green child with a minimum and maximum width should get a width of 75 proposed first, becoming its minimum width of 90. Then the red child should get the remaining width of 60 proposed, taking on that width since it only has a maximum width constraint of 100.

However, the red rectangle becomes 75 wide. SwiftUI seems to propose to the red child first. Clearly, this is either a bug in SwiftUI, or an error in our understanding. (Spoiler: it's the latter!).

The truth of HStack 's view sorting seems to be much simpler: the flexibility of a view is defined by the range of the widths it can become .

In the example above, the red child can become anywhere between 0 and 100 points wide, hence it has a flexibility of 100. The green child can become anywhere between 90 and 200 points wide, hence it has a flexibility of 110.

This also explains why our initial, slightly over complicated heuristic worked in most cases:

  • a fixed size view has a flexibility of 0.

  • a view with both minWidth and maxWidth has a flexibility of maxWidth-minWidth .

  • a view with just a maxWidth has a flexibility of 0...maxWidth .

  • a view with just a minWidth has the flexibility of minWidth...CGFloat.someLargeNumber

A view with just a minimum width will always come last in practice, because the flexibility of minWidth...CGFloat.someLargeNumber is very large. Fixed size views will always come first, due to their flexibility of 0. However, the sorting of views with just a maximum width and views with both minimum and maximum widths were mixed up in our initial approach.

Note: we wrote CGFloat.someLargeNumber instead of CGFloat.greatestFiniteMagnitude . The number has to be large enough, but not so large that we lose precision.

On Swift Talk, our weekly video series, we have been reimplementing all the important parts of SwiftUI's layout system as a way to fully understand its behavior — as always, the first episode of the series is free to watch.

In an upcoming episode, we will implement exactly the algorithm above for HStacks, so stay tuned!

For those who prefer reading, our book Thinking in SwiftUI includes chapters on view layout, including the layout algorithm, and how to build advanced layouts.