2

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)
                    }
            }
        }
    }
}
alionthego
  • 8,508
  • 9
  • 52
  • 125
  • 3
    If you used `.onChange(of: geometry.size) { ... }` it would work – New Dev Apr 30 '21 at 03:05
  • Think of UIKit's `viewDidLoad`... usually, the frames are never right on the first load. Just like you would modify the frames in `viewDidLayoutSubviews`, you would modify them inside `.onChange` as @NewDev suggested – aheze Apr 30 '21 at 03:44
  • I'm really not following, I don't want to just print the size. I want the subviews to be laid out correctly on the first pass. Also where would I use .onChange(of: geometry.size) . Can you please help me to understand better how this works? Thanks. – alionthego Apr 30 '21 at 03:58

1 Answers1

2

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.

wristbands
  • 1,021
  • 11
  • 22