< Absolute positioning for SwiftUI views | Understanding frames and coordinates inside GeometryReader > |
SwiftUI lets us create views with exact sizes like this:
Image(.example)
.resizable()
.scaledToFit()
.frame(width: 300, height: 300)
All this works great if we want fixed-sized views, but very often you want images that automatically scale up to fill more of the screen in one or both dimensions. That is, rather than hard-coding a width of 300, what you really want to say is “make this image fill 80% of the width of the screen.”
One option is to use the containerRelativeFrame()
modifier, which we covered back in project 8. But
SwiftUI also gives us a dedicated type for this work called GeometryReader
, and it’s remarkably powerful.
We’ll go into much more detail on GeometryReader
shortly, but for now we’re going to use it for one job: to make sure our image fills some percentage of its container's width.
GeometryReader
is a view just like the others we’ve used, except when we create it we’ll be handed a GeometryProxy
object to use. This lets us query the environment: how big is the container? What position is our view? Are there any safe area insets? And so on.
In principle that seems simple enough, but in practice you need to use GeometryReader
carefully because it automatically expands to take up available space in your layout, then positions its own content aligned to the top-left corner.
For example, we could make an image that’s 80% the width of the screen, with a fixed height of 300:
GeometryReader { proxy in
Image(.example)
.resizable()
.scaledToFit()
.frame(width: proxy.size.width * 0.8, height: 300)
}
You can even remove the height
from the image, like this:
GeometryReader { proxy in
Image(.example)
.resizable()
.scaledToFit()
.frame(width: proxy.size.width * 0.8)
}
We’ve given SwiftUI enough information that it can automatically figure out the height: it knows the original width, it knows our target width, and it knows our content mode, so it understands how the target height of the image will be proportional to the target width.
Now, you're probably wondering how this is any different from using containerRelativeFrame()
. Well, the problem is that containerRelativeFrame()
has a very precise definition of what constitutes a "container": it might be the whole screen, it might be a NavigationStack
, it might be a List
or a ScrollView
, and so on, but it won't consider a HStack
or a VStack
a container.
This causes problems when using views in stacks, because you can't easily subdivide them using containerRelativeFrame()
. For example, the code below places two views in a HStack
, with one being given a fixed width and the other using a container relative frame:
HStack {
Text("IMPORTANT")
.frame(width: 200)
.background(.blue)
Image(.example)
.resizable()
.scaledToFit()
.containerRelativeFrame(.horizontal) { size, axis in
size * 0.8
}
}
That's not going to lay out well at all, because the containerRelativeFrame()
will read the whole screen width for its size, meaning that image will be 80% the screen width despite 200 points of the screen being a text view.
On the other hand, using a GeometryReader
will subdivide the space correctly:
GeometryReader { proxy in
Image(.example)
.resizable()
.scaledToFit()
.frame(width: proxy.size.width * 0.8)
}
Of course, that introduces a different problem: our image is now aligned to the top-left corner of the GeometryReader
!
Fortunately, this is easily solved. If you ever want to center a view inside a GeometryReader
, rather than aligning to the top-left corner, add a second frame that makes it fill the full space of the container, like this:
GeometryReader { proxy in
Image(.example)
.resizable()
.scaledToFit()
.frame(width: proxy.size.width * 0.8)
.frame(width: proxy.size.width, height: proxy.size.height)
}
SPONSORED Transform your career with the iOS Lead Essentials. This Black Friday, unlock over 40 hours of expert training, mentorship, and community support to secure your place among the best devs. Click for early access to this limited offer and a free crash course.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.