'GeometryReader with NavigationView in SwiftUI is initially giving .zero for size

I have a GeometryReader in a NavigationView and initially the size is 0 when the view first displayed. I'm not sure if it's a bug or the correct behavior but I'm looking for a way to solve this as my child views are not rendering correctly.

This struct demonstrates the problem.

This printout from below is: (0.0, 0.0) for size.

Is there anyway to force the NavigationView to provide correct geometry when initially displayed?

struct ContentView: View {
    var body: some View {
        NavigationView {
            GeometryReader { geometry in
                Text("Geometry Size Is Wrong")
                    .onAppear {
                        print(geometry.size)  // prints out (0.0, 0.0)
                    }
            }
        }
    }
}


Solution 1:[1]

Unfortunately, I don't think there's anything you can do to make NavigationView provide the correct geometry when initially displayed.

But if you do want access to the final geometry.size from within your view, you can use onChange(of:) as New Dev suggested:

struct ContentView: View {
  @State var currentSize: CGSize?

  var body: some View {
    NavigationView {
      GeometryReader { geometry in
        Text("currentSize will soon be correct")
          .onChange(of: geometry.size) { newSize in
            currentSize = newSize
            print(currentSize!) // prints (320.0, 457.0)
          }
      }
    }
  }
}

The above will work fine for many cases, but note that any local variables computed from geometry.size within the GeometryReader's subviews will not be accurate in the onChange block (it will capture the original, wrong value):

struct ContentView: View {
  @State var currentSize: CGSize?
  @State var halfWidth: CGFloat?

  var body: some View {
    NavigationView {
      GeometryReader { geometry in
        let halfWidthLocal = geometry.size.width / 2

        Text("Half Width is really: \(halfWidthLocal)") // will read as "Half Width is really 160.000000"
          .onChange(of: geometry.size) { newSize in
            currentSize = newSize
            halfWidth = halfWidthLocal
            print(currentSize!) // prints (320.0, 457.0)
            print(halfWidth!) // prints 0.0
          }
      }
    }
  }
}

In order to update state properties using the most up-to-date version of local variables, you can instead update the properties within a function that returns a view in your GeometryReader:

struct ContentView: View {
  @State var currentSize: CGSize?
  @State var halfWidth: CGFloat?

  var body: some View {
    NavigationView {
      GeometryReader { geometry in
        let halfWidthLocal = geometry.size.width / 2

        makeText(halfWidthLocal: halfWidthLocal)
          .onChange(of: geometry.size) { newSize in
            currentSize = newSize
            print(currentSize!) // prints (320.0, 457.0)
          }
      }
    }
  }

  func makeText(halfWidthLocal: CGFloat) -> some View {
    DispatchQueue.main.async { // Must update state properties on the main queue
      halfWidth = halfWidthLocal
      print(halfWidth!) // prints 0.0 the first time, then 160.0 the second time
    }
    return Text("Half Width is really: \(halfWidthLocal)") // will read as "Half Width is really 160.000000"
  }
}

This type of situation came up for me, so just thought I'd pass on the knowledge to others.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 wristbands