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 HStack
s, VStack
s, and Spacer
s work, right?
Let’s position this view:
let content = Text("Position me!")
Leading
// This...
HStack(spacing: 0) {
content
Spacer()
}
// ...is the same as
content
.frame(maxWidth: .infinity, alignment: .leading)
Trailing
// This...
HStack(spacing: 0) {
Spacer()
content
}
// ...is the same as
content
.frame(maxWidth: .infinity, alignment: .trailing)
Center
// This...
HStack(spacing: 0) {
Spacer()
content
Spacer()
}
// ...is the same as
content
.frame(maxWidth: .infinity) // Center alignment is the default!
Bottom
// This...
VStack(spacing: 0) {
Spacer()
content
}
// ...is the same as
content
.frame(maxHeight: .infinity, alignment: .bottom)
Bottom Trailing
// 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 Spacer
s 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, Spacer
s 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 UIView
s and NSView
s, 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:
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 a frame’s “ideal size” is infinity, you would expect it to take all the available space, but it doesn’t. That’s because its “ideal” size is what Auto Layout called an “intrinsic content size”, which was a much better name.
- Similarly, when people say a thing must have a “fixed size”, they mean they always want it to be that size no matter what.
.fixedSize()
instead “fixes this view at its ideal size”, which is very much not that! - A frame’s max size can be exceeded by specifying a higher ideal size (really, try it), which makes it… not be a max size?
- Adding a max dimension constraint will result in a frame that is as big or bigger than it would’ve been otherwise. Read that again.
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 #Preview
s). Of these:
- 23 are of the simpler
(width:height:)
variety. - 5 are straightforward cases of only using min dimensions.
- 39 are like the examples in this article.
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!