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)
        }
    }
}