Home > Back-end >  SwiftUI - How to make NavigationView not affect geometry reader's location
SwiftUI - How to make NavigationView not affect geometry reader's location

Time:03-24

I'm trying to get the position of a scrollview using a geometry reader. However, I've noticed that it gives me a different value depending on if I have it embedded in a NavigationView or not. Here is an example:

struct ContentView: View {
    @State var headerLocation: CGFloat = 0
    var body: some View {
        ZStack(alignment: .top) {
            Color.white
                .ignoresSafeArea()
            VStack(spacing: 0) {
                ZStack {
                    Text("test.username")
                        .font(.custom("Helvetica Bold", size: 18))
                }
                .frame(height: 40)
                .overlay(alignment: .bottom) {
                    GeometryReader { geo -> Color in
                        let minY = geo.frame(in: .global).minY
                        DispatchQueue.main.async {
                            if headerLocation == 0 {
                                headerLocation = minY
                            }
                        }
                        return Color.blue
                    }
                    .frame(width: 100, height: 1)
                }
                ScrollView {
                    VStack(spacing: 8) {
                        Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut eu sem integer vitae. Pretium aenean pharetra magna ac. Vel pretium lectus quam id leo in vitae. Molestie nunc non blandit massa enim nec dui nunc mattis. Ornare quam viverra orci sagittis eu volutpat odio facilisis. Ultrices neque ornare aenean euismod elementum nisi quis. Non diam phasellus vestibulum lorem sed. Platea dictumst quisque sagittis purus sit amet volutpat consequat mauris. Dapibus ultrices in iaculis nunc sed augue lacus viverra.")
                        GeometryReader { proxy -> Color in
                            let offset = proxy.frame(in: .global).minY
                            print("\(offset), \(headerLocation)")
                            return Color.green
                        }
                        .frame(width: 40, height: 1)
                        .padding(.top, 10)

                        Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut eu sem integer vitae. Pretium aenean pharetra magna ac. Vel pretium lectus quam id leo in vitae. Molestie nunc non blandit massa enim nec dui nunc mattis. Ornare quam viverra orci sagittis eu volutpat odio facilisis. Ultrices neque ornare aenean euismod elementum nisi quis. Non diam phasellus vestibulum lorem sed. Platea dictumst quisque sagittis purus sit amet volutpat consequat mauris. Dapibus ultrices in iaculis nunc sed augue lacus viverra.")
                        Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut eu sem integer vitae. Pretium aenean pharetra magna ac. Vel pretium lectus quam id leo in vitae. Molestie nunc non blandit massa enim nec dui nunc mattis. Ornare quam viverra orci sagittis eu volutpat odio facilisis. Ultrices neque ornare aenean euismod elementum nisi quis. Non diam phasellus vestibulum lorem sed. Platea dictumst quisque sagittis purus sit amet volutpat consequat mauris. Dapibus ultrices in iaculis nunc sed augue lacus viverra.")
                        Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut eu sem integer vitae. Pretium aenean pharetra magna ac. Vel pretium lectus quam id leo in vitae. Molestie nunc non blandit massa enim nec dui nunc mattis. Ornare quam viverra orci sagittis eu volutpat odio facilisis. Ultrices neque ornare aenean euismod elementum nisi quis. Non diam phasellus vestibulum lorem sed. Platea dictumst quisque sagittis purus sit amet volutpat consequat mauris. Dapibus ultrices in iaculis nunc sed augue lacus viverra.")

                        Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut eu sem integer vitae. Pretium aenean pharetra magna ac. Vel pretium lectus quam id leo in vitae. Molestie nunc non blandit massa enim nec dui nunc mattis. Ornare quam viverra orci sagittis eu volutpat odio facilisis. Ultrices neque ornare aenean euismod elementum nisi quis. Non diam phasellus vestibulum lorem sed. Platea dictumst quisque sagittis purus sit amet volutpat consequat mauris. Dapibus ultrices in iaculis nunc sed augue lacus viverra.")
                    }
                }
            }
        }
    }
}

The code above functions like intended. When the app is started, it sets the location of the blue rectangle at the top of the screen. If we scroll the scrollview, we can see that the number is equal when we scroll the green rectangle to the same location as the blue one.

Now take the code below:

struct ContentView: View {
    @State var headerLocation: CGFloat = 0
    var body: some View {
        NavigationView {
            ZStack(alignment: .top) {
                Color.white
                    .ignoresSafeArea()
                VStack(spacing: 0) {
                    ZStack {
                        Text("test.username")
                            .font(.custom("Helvetica Bold", size: 18))
                    }
                    .frame(height: 40)
                    .overlay(alignment: .bottom) {
                        GeometryReader { geo -> Color in
                            let minY = geo.frame(in: .global).minY
                            DispatchQueue.main.async {
                                if headerLocation == 0 {
                                    headerLocation = minY
                                }
                            }
                            return Color.blue
                        }
                        .frame(width: 100, height: 1)
                    }
                    ScrollView {
                        VStack(spacing: 8) {
                            Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut eu sem integer vitae. Pretium aenean pharetra magna ac. Vel pretium lectus quam id leo in vitae. Molestie nunc non blandit massa enim nec dui nunc mattis. Ornare quam viverra orci sagittis eu volutpat odio facilisis. Ultrices neque ornare aenean euismod elementum nisi quis. Non diam phasellus vestibulum lorem sed. Platea dictumst quisque sagittis purus sit amet volutpat consequat mauris. Dapibus ultrices in iaculis nunc sed augue lacus viverra.")
                            GeometryReader { proxy -> Color in
                                let offset = proxy.frame(in: .global).minY
                                print("\(offset), \(headerLocation)")
                                return Color.green
                            }
                            .frame(width: 40, height: 1)
                            .padding(.top, 10)

                            Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut eu sem integer vitae. Pretium aenean pharetra magna ac. Vel pretium lectus quam id leo in vitae. Molestie nunc non blandit massa enim nec dui nunc mattis. Ornare quam viverra orci sagittis eu volutpat odio facilisis. Ultrices neque ornare aenean euismod elementum nisi quis. Non diam phasellus vestibulum lorem sed. Platea dictumst quisque sagittis purus sit amet volutpat consequat mauris. Dapibus ultrices in iaculis nunc sed augue lacus viverra.")
                            Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut eu sem integer vitae. Pretium aenean pharetra magna ac. Vel pretium lectus quam id leo in vitae. Molestie nunc non blandit massa enim nec dui nunc mattis. Ornare quam viverra orci sagittis eu volutpat odio facilisis. Ultrices neque ornare aenean euismod elementum nisi quis. Non diam phasellus vestibulum lorem sed. Platea dictumst quisque sagittis purus sit amet volutpat consequat mauris. Dapibus ultrices in iaculis nunc sed augue lacus viverra.")
                            Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut eu sem integer vitae. Pretium aenean pharetra magna ac. Vel pretium lectus quam id leo in vitae. Molestie nunc non blandit massa enim nec dui nunc mattis. Ornare quam viverra orci sagittis eu volutpat odio facilisis. Ultrices neque ornare aenean euismod elementum nisi quis. Non diam phasellus vestibulum lorem sed. Platea dictumst quisque sagittis purus sit amet volutpat consequat mauris. Dapibus ultrices in iaculis nunc sed augue lacus viverra.")

                            Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut eu sem integer vitae. Pretium aenean pharetra magna ac. Vel pretium lectus quam id leo in vitae. Molestie nunc non blandit massa enim nec dui nunc mattis. Ornare quam viverra orci sagittis eu volutpat odio facilisis. Ultrices neque ornare aenean euismod elementum nisi quis. Non diam phasellus vestibulum lorem sed. Platea dictumst quisque sagittis purus sit amet volutpat consequat mauris. Dapibus ultrices in iaculis nunc sed augue lacus viverra.")
                        }
                    }
                }
            }
            .navigationTitle("")
            .navigationBarTitleDisplayMode(.inline)
            .navigationBarHidden(true)
        }
    }
}

When we try to do the same thing, we can see that when the green rectangle is at the same location as the blue one, they have different values. How can I achieve the functionality of the first code block while keeping a NavigationView? I've already tried the different NavigationBarDisplayModes and NavigationTitles

CodePudding user response:

Let me introduce you to .coordinateSpace() where you can pick the view you want to use to compare GeometryReaders. Simply put, as you can see, the NavigationView is messing with the values in the GeometryReaders, so since they both exist in the ZStack, we can easily compare them there. The trick is telling the GeometryReaders what view to use. We do this with .coordinateSpace() placed on the ZStack, and then, instead of using .global() we use .named(). I have abbreviated your Text() to make it shorter and clearer. Here is the code at work:

struct ContentView: View {
    @State var headerLocation: CGFloat = 0
    var body: some View {
        NavigationView {
            ZStack(alignment: .top) {
                Color.white
                    .ignoresSafeArea()
                VStack(spacing: 0) {
                    ZStack {
                        Text("test.username")
                            .font(.custom("Helvetica Bold", size: 18))
                    }
                    .frame(height: 40)
                    .overlay(alignment: .bottom) {
                        GeometryReader { geo -> Color in
                            //Use .named() instead of .global()
                            let minY = geo.frame(in: .named("ZStack")).minY
                            DispatchQueue.main.async {
                                if headerLocation == 0 {
                                    headerLocation = minY
                                }
                            }
                            return Color.blue
                        }
                        .frame(width: 100, height: 1)
                    }
                    ScrollView {
                        VStack(spacing: 8) {
                            Text("Lorem.")
                            GeometryReader { proxy -> Color in
                                //Use .named() instead of .global()
                                let offset = proxy.frame(in: .named("ZStack")).minY
                                print("\(offset), \(headerLocation)")
                                return Color.green
                            }
                            .frame(width: 40, height: 1)
                            .padding(.top, 10)

                            Text("Lorem.")
                            Text("Lorem.")
                            Text("Lorem.")
                            Text("Lorem.")
                        }
                    }
                }
            }
            // Put the .coordinateSpace here. The name can be any hashable value.
            .coordinateSpace(name: "ZStack")
            .navigationTitle("")
            .navigationBarTitleDisplayMode(.inline)
            .navigationBarHidden(true)
        }
    }
}
  • Related