For most layout needs we can combine stacks and flexible frames, allowing us to make views and controls put together from fixed size primitives views upwards.
For more complex layout needs, another option is to use
GeometryReader
. This is a construct that acts like an infinite frame,
proposing the size of its parent to its children, and choosing its
parent size as its own.
As an added feature, it passes the proposed size received to the builder as a closure argument. For example we can layout an image at a maximum of half of the size of the parent, while maintaining aspect ratio, with:
struct ContentView : View {
var body: some View {
GeometryReader { g in
ZStack {
Image("barbarian")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: g.size.width / 2,
maxHeight: g.size.height / 2)
}
.frame(width: g.size.width, height: g.size.height)
}
}
}
The GeometryReader
acts exactly like the infinite frame we saw in
flexible frames, it
proposes the size of its parent to its child, but it also passes that
proposed size g
to our view builder.
And just like the infinite frame, the geometry reader doesn’t use the size of the child when deciding its own size; instead it always returns the proposed size from the parent as its own size.
An important gotcha is that within the view builder we need to do our
own sizing, positioning and alignment; ZStack
is perfect for this. We
still need to position that, so we place it inside a frame that has the
same size as the reader parent, and let the stack be centered inside it.
Finally inside the stack we place our image, and place that inside a frame that constrains its width and height to half the size of the reader.
In Secondary Views
With access to the proposed size of the parent, GeometryReader
can
seem powerful, but the resulting fixed size equally that can limit their
usefulness. When combined with secondary
views they become even
more convenient.
As we saw above, when free floating, a geometry reader expands to fill the size proposed by the parent.
But because the proposed parent size of a secondary view is the fixed
size decided by the view its attached to, that is the proposed size.
Thus GeometryReader
inside a secondary view returns the size of the
view it’s attached to.
`GeometryReader` inside a secondary view returns the size of the view it's attached to
For clarity I like to separate out complex secondary views into a separate view:
struct OverlaidImage : View {
var body: some View {
Image("barbarian")
.resizable()
.aspectRatio(contentMode: .fit)
.overlay(OverlayView())
}
}
struct OverlayView : View {
var body: some View {
GeometryReader { g in
Image("overlay")
.resizable()
.frame(width: g.size.width, height: g.size.height / 2)
.position(y: g.size.height / 2)
}
}
}
In this example the Image
in the body is allowed to be resizable while
maintaining its own aspect ratio, with the ultimate bounds of that
determined by whatever uses our OverlaidImage
custom view.
We then use an .overlay
secondary view to draw another image over the
bottom half of that image.
In order to constrain that to the bottom half we need to know the size
of the Image
we’re drawing over, and the GeometryReader
works for
this because it’s in the secondary view.
In ZStacks
As we saw in stacks and secondary views, the z-axis stack has a useful property where it’s only the children with the highest layout priority that influence the size of the stack, and those with lower priorities receive that size as a proposed size.
This can be usefully combined with GeometryReader
just as we can with
a secondary view, and can often produce more readable results.
Consider the above example, reformulated using a ZStack
with the
GeometryReader
given a lower layout priority:
struct OverlaidImage : View {
var body: some View {
ZStack {
GeometryReader { g in
Image("overlay")
.resizable()
.frame(width: g.size.width, height: g.size.height / 2)
.position(y: g.size.height / 2)
}
.layoutPriority(-1)
Image("barbarian")
.resizable()
.aspectRatio(contentMode: .fit)
}
}
}