GeometryReader with NavigationView in SwiftUI is initially giving .zero for size
Asked Answered
I

2

6

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)
                    }
            }
        }
    }
}
Illinois answered 30/4, 2021 at 2:29 Comment(3)
If you used .onChange(of: geometry.size) { ... } it would workConfessor
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 suggestedCogon
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.Illinois
S
4

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.

Salba answered 12/3, 2022 at 20:7 Comment(0)
R
0

Views inside a navigation stack are initially rendered with size 0, then immediately re-rendered at non-zero size. Because .onAppear() closures run only once, your code there sees only the zero, not the subsequent actual size value. And it's pretty common in SwiftUI to see views being multiply rendered for many reasons.

But you can wait to render the view inside GeometryReader until the geometry is nonzero. Thus your .onAppear() won't run at all until the geometry is good, or at least nonzero(*). Just condition drawing your view on geometry.size != .zero

For your example code that could look like this:

struct ContentView: View {
var body: some View {
    NavigationView {
        GeometryReader { geometry in

            if geometry.size != .zero {

                Text("Geometry Size Is \(geometry.size.width) x \(geometry.size.height)")
                    .onAppear {
                        print(geometry.size)  // prints out a nonzero size
                    }
                }

            }
        }
    }
}

(*) Caveat: If you add animations to your navigation transitions this might not/won't get you the final geometry because your view will be drawn many times at various sizes.

Retardment answered 21/3 at 13:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.