Search code examples
swiftui-navigationlinkswiftui

onReceive(self.timer) doesn't work inside a NavigationView


I have this page of a Scrollview with a custom header only shown when it scrolls past a certain height. I use GeometryReader with onReceive to constantly check the current scrolling height:

@State var userInfoUpateInterval = Timer.publish(every: 0.1, on: .current, in: .tracking).autoconnect()
@State var showHeader: Bool = false

var body: some View {
    NavigationView {
        ZStack(alignment: .top) {
            ScrollView(.vertical) {
                GeometryReader { geometry in
                    Text("User info component").onReceive(self.userInfoUpateInterval) { (_) in
                        self.onUserInfoLayoutChange(geometry)
                    }
                }
                
                VStack {
                    Text("content")
                }.frame(width: UIScreen.screenWidth, height: 1500)
                
            }
        
            ProfileHeader(title: "user.userName", showHeader: $showHeader)
        }
    }
}

The scrolling and header hiding/showing works perfectly until I wrapped the ZStack in a NavigationView. onReceive is simply not triggered anymore. If I swap NavigationView with a ZStack everything works as expected again.

I have seen this Timer onReceive not working inside NavigationView question but I don't have conditional component. Is this a SwiftUI bug or I'm doing something wrong?


Solution

  • Here is a demo of possible solution for your case. Tested with Xcode 11.4 / iOS 13.4 (and it is forward compatible)

    The idea is to react not by timer but by view position change that has been read/tracked by view preferences.

    enter image description here

    struct ViewOffsetKey: PreferenceKey {
        typealias Value = CGFloat
        static var defaultValue: CGFloat { 0 }
        static func reduce(value: inout Value, nextValue: () -> Value) {
            value = value + nextValue()
        }
    }
    
    
    struct DemoView: View {
        @State var showHeader: Bool = false
    
        var body: some View {
            NavigationView {
                ZStack(alignment: .top) {
                    ScrollView(.vertical) {
                        Text("User info component")
                            .background(GeometryReader {
                                Color.clear.preference(key: ViewOffsetKey.self,
                                                       value: -$0.frame(in: .named("scroll_area")).origin.y) })
                        VStack {
                            Text("content")
                        }.frame(maxWidth: .infinity, minHeight: 1500)
                    }.coordinateSpace(name: "scroll_area")
    
                    if showHeader {
                        Text("ProfileHeader")
                    }
                }
            }
            .onPreferenceChange(ViewOffsetKey.self) {
                self.showHeader = $0 > 200    // << your condition
            }
        }
    }