SwiftUI’s .frame Modifier Explained With Stacks.

I bet I can teach you how to layout views with this modifier in two steps!

Step 1 – What It Does

.frame does NOT set or modify a View’s dimensions.

It puts a View inside an invisible container, which we’ll call a “frame”.

.frame is an invisible container, just like stacks.

Step 2 – How It Works

All that’s left to figure out is where inside the frame our view will end up.

You know how HStacks, VStacks, and Spacers work, right?

Let’s position this view:

let content = Text("Position me!")

Leading

Leading frame

// This...
HStack(spacing: 0) {
  content
  Spacer()
}
// ...is the same as
content
  .frame(maxWidth: .infinity, alignment: .leading)

Trailing

Trailing frame

// This...
HStack(spacing: 0) {
  Spacer()
  content
}
// ...is the same as
content
  .frame(maxWidth: .infinity, alignment: .trailing)

Center

Center frame

// This...
HStack(spacing: 0) {
  Spacer()
  content
  Spacer()
}
// ...is the same as
content
  .frame(maxWidth: .infinity) // Center alignment is the default!

Bottom

Bottom frame

// This...
VStack(spacing: 0) {
  Spacer()
  content
}
// ...is the same as
content
  .frame(maxHeight: .infinity, alignment: .bottom)

Bottom Trailing

Bottom trailing frame

// And this...
VStack(spacing: 0) {
  Spacer()
  HStack(spacing: 0) {
    Spacer()
    content
  }
}
// ...is the same as
content
  .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)

So, did I do it?

Of course, if you don’t want the frame to expand forever like Spacers do, you can give it max dimensions lower than .infinity. If you don’t want it to expand at all, and want it to have a fixed size instead, use the .frame(width:height:alignment:) overload.

As a rule of thumb, Spacers are useful to create space between views. If you’re using a Spacer with nothing on one side, you should probably use a frame instead.

Why It’s Confusing

Well for one, it has the same name as the frame property in UIViews and NSViews, but very different behavior. So if you’re coming from UIKit or AppKit, you have some unlearning to do.

But I think devs (me absolutely included) might struggle understanding .frame because it behaves like a container, but it looks like a modifier.

While it is true that, strictly speaking, all SwiftUI modifiers are containers/wrappers, you could argue that most of them conceptually modify the view they are attached to, while .frame definitely does not.

Consider this alternative API:

Trailing frame

Frame(maxWidth: .infinity, alignment: .trailing) {
  content
}

Would you consider this more intuitive? …I bet this article wouldn’t exist if this was real.

A Little Rant You Can Skip

The documentation says:

Note that most alignment values have no apparent effect when the size of the frame happens to match that of this view.

Notes like these are typically a sign that you have an issue with your API’s design or naming. And guess what, I have a few other gripes with it!

Like:

If you take the time to decipher the “Discussion” section of the docs, you can understand its behavior and confirm it in practice – in other words, none of these complaints are due to bugs. The API is working as intended. It’s just that its intended behavior is so byzantine it defies expectations.

I’m not sure what went wrong here. I’d say the rest of SwiftUI is really well designed, including stack-based layout. By and large, things have sensible naming and behavior. Maybe this API is trying to do too much. Maybe it had to be like this – because of a Swift limitation that will be lifted in the future, or for performance reasons. Maybe the problem it’s solving is intrinsically very complex and I’m just not smart enough.

But sometimes I miss Auto Layout.

Anyways

As you can see, once you understand .frame you can make common layout code less verbose, and more readable – for yourself, at least.

At the time of writing, this modifier has 68 uses in Wipr 2’s codebase (excluding #Previews). Of these:

Which leaves a grand total of 1 call I haven’t prepared you for :) That’s probably good enough for today.

Check out my devlog for more tips. Happy laying out!