swiftswiftui

Why does `GeometryReader` return a smaller size during window resize while the view visually retains its size?


I am using GeometryReader in SwiftUI to track the frame of a Color.black background. However, when I resize the window, the size retrieved from GeometryReader (via geo.frame(in: .local)) unexpectedly shrinks, even though the Color.black background visually remains the correct size.

Here’s the code I am using:

struct ContentView: View {
  @State private var rect: CGRect = CGRect()

  var body: some View {
    GeometryReader { geo in
      ZStack {
        Color.black
          .onAppear {
            rect = geo.frame(in: .local)
          }
          .onChange(of: geo.frame(in: .local)) { oldValue, newValue in
            rect = newValue
          }

        Color.red.frame(width: rect.width, height: 2.0)
        Color.blue.frame(width: 2.0, height: rect.height)
        Color.green
          .frame(width: 300.0, height: 50.0)
          .offset(y: -100.0)
      }
    }
    .padding(300.0)
  }
}

Why is the size value from the GeometryReader shrinking during a window resize, and how can I ensure it always matches the size of the Color.black background?


Solution

  • This is because the size of the GeometryReader is more flexible than the size of the Color.black. It can change to a size that is smaller than the Color.black. The Color.black is always as large as the ZStack, which is at least as large as the Color.green.

    You can see the size of the GeometryReader by adding a .border(.red) to the it. before the padding.

    So one way to solve this is just not to consider the cases where the size of the GeometryReader is smaller than the Color.black. It's not very clear what should the view look like when that happens, anyway. You can do this by setting a minimum size:

    GeometryReader { geo in
        // ...
    }
    .frame(minWidth: 300, minHeight: 50)
    .padding(300.0)
    

    If you want the Color.black to be resizable to a size smaller than the Color.green, do not put them in the same ZStack. Use an overlay/background to put one on top of/below the other.

    GeometryReader { geo in
        Color.black
            .onAppear {
                rect = geo.frame(in: .local)
            }
            .onChange(of: geo.frame(in: .local)) { oldValue, newValue in
                rect = newValue
            }
            .overlay {
                ZStack {
                    Color.red.frame(width: rect.width, height: 2.0)
                    Color.blue.frame(width: 2.0, height: rect.height)
                    Color.green
                        .frame(width: 300.0, height: 50.0)
                        .offset(y: -100.0)
                }
            }
    }
    .padding(300.0)
    

    Other notes:

    Color.black
        // .frame(minWidth: 300, minHeight: 50) // you can still add this if you want
        .onGeometryChange(for: CGRect.self) {
            $0.frame(in: .local)
        } action: { newValue in
            rect = newValue
        }
        .overlay {
            ZStack {
                Color.red.frame(width: rect.width, height: 2.0)
                Color.blue.frame(width: 2.0, height: rect.height)
                Color.green
                    .frame(width: 300.0, height: 50.0)
                    .offset(y: -100.0)
            }
        }
        .padding(300.0)