Getting view size in SwiftUI without GeometryReader

We can avoid GeometryReader now! New modifier supports iOS 16+.

Published: Nov. 26, 2024
App Store

Recently I needed to know size of SwiftUI container to layout horizontal carousel. The reason was to have one items always at least a tiny bit visible so users can reliably find the rest by scrolling. I started with GeometryReader and quickly got frustrated and started looking for another solution…

There are many problems with GeometryReader and its behavior, which is why I am trying to avoid it whenever I can.

Anyway… I started looking for alternative sort of expecting to find nothing or perhaps something for iOS 18, but I need to support iOS 16+. However I did discover modifier onGeometryChanged.

This is actually new with iOS 18 (Xcode 16) but it has been backported all the way to iOS 16. It will tell you when the frame or size of your view changes. Which means you can use this in @State and update child views accordingly.

The new modifier also makes it easy to create layout loops, so be careful. If I wanted to modify size of the view that has its geometry tracked, that would again invoke the geometry changed and so on.

The onGeometryChanged approach also seems to work well for “sized to content bottom sheets”. Below is super simple example but it should show all the relevant parts.

SwiftUI bottom sheet based on content example

You need property to hold the height and then these modifiers:

SheetContentView()
    .onGeometryChange(for: CGSize.self) { proxy in
         proxy.size
     } action: {
         self.contentHeight = $0.height
         // Alternatively you can get the `width` here
     }
     .presentationDetents([.height(contentHeight)])

Here is entire code you can copy and run:

import SwiftUI

struct ContentView: View {
    @State private var contentHeight: CGFloat = 0
    @State private var showsSheet = false
    @State private var fontSize: CGFloat = 50

    var body: some View {
        VStack(spacing: 20) {
            Text("onGeometryChange example")
                .font(.largeTitle)
                .multilineTextAlignment(.center)

            Button {
                fontSize = CGFloat.random(in: 30...80)

                showsSheet = true
            } label: {
                Text("Show sheet")
            }
            .buttonStyle(.bordered)

        }
        .padding()
        .sheet(isPresented: $showsSheet) {
            VStack {
                Text("As you can see this sheet is dynamically sized to fit this content.")
                    .fixedSize(horizontal: false, vertical: true)
                    .padding()
                    .font(.system(size: fontSize))

            }
            .onGeometryChange(for: CGSize.self) { proxy in
                proxy.size
            } action: {
                self.contentHeight = $0.height
                // Alternatively you can get the `width` here
            }
            .presentationDetents([.height(contentHeight)])

        }
    }
}
Bluesky logo

Follow on Bluesky to not miss new posts

Filip Němeček profile photo

WRITTEN BY

Filip Němeček Mastodon

iOS blogger and developer with interest in Python/Django.

iOS blogger and developer with interest in Python/Django.