Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

onAppear called immediately after push/pop #19

Closed
spifd opened this issue May 30, 2020 · 4 comments
Closed

onAppear called immediately after push/pop #19

spifd opened this issue May 30, 2020 · 4 comments

Comments

@spifd
Copy link

spifd commented May 30, 2020

Hi there,

First, thanks for this stack, really useful for my small side project as a SwiftUI rookie.

Actually, I've just encountered the following behavior: onAppear is called immediately when pushing/popping pages, and not when the transition is finished. As I have an advanced custom transition taking more time and leading to a page that will have its own animation, I would like onAppear to be delayed until the transition is finished.

I believe this is not really a bug but I'm wondering if this could be solved by this stack. Note: I can probably use state and enhance my custom transition to workaround (even though tbh custom transitions are a bit buggy when I want to deal with timing/delays).

Here is a simple example:

struct DestinationView: View {
    var body: some View {
        VStack {
            Spacer()
            HStack {
                PopView {
                    Text("Page 2").onAppear {
                        print("Page 2 onAppear") // Printed immediately
                    }
                }
            }
            Spacer()
        }
        .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
    }
}

struct Transition_Previews: PreviewProvider {
    static var previews: some View {
        VStack {
            NavigationStackView(transitionType: .default,
                                easing: Animation.linear(duration: 5)) {
                                    PushView(destination: DestinationView()) {
                                        HStack {
                                            Spacer()
                                            Text("Page 1").onAppear {
                                                print("Page 1 onAppear") // Printed immediately
                                            }
                                            Spacer()
                                        }
                                    }
            }
        }
        .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
    }
}
@spifd
Copy link
Author

spifd commented Jun 2, 2020

Hi,

For now, I've used the following method introducing a new onNavigationComplete function to replace onAppear as seamlessly as possible. This is done using a modifier that allows back and forth navigation views to communicate through an ObservableObject when onDisappear is called. However, to be honest, I'm not yet very familiar with Combine and SwiftUI yet. So there may be more elegant proven solution, in particular, to avoid having a UUID in each view.

    class NavigationContext: ObservableObject {
        @Published var originViewId: UUID?
    }

    extension View {
        func onNavigationComplete(_ id: UUID, perform action: (() -> Void)? = nil) -> some View {
            modifier(NavigationTrackModifier(id: id, action: action))
        }
    }

    struct NavigationTrackModifier: ViewModifier {
        var id: UUID

        var action: (() -> Void)?

        @EnvironmentObject var navContext: NavigationContext

        func body(content: Content) -> some View {
            content
                .onDisappear {
                    self.navContext.originViewId = self.id
                }
                .onReceive(navContext.$originViewId) { viewId in
                    if viewId != nil, viewId != self.id {
                        self.action?()
                        self.navContext.originViewId = nil
                    }
                }
        }
    }

    struct DestinationView: View {
        var id = UUID()

        var body: some View {
            VStack {
                PopView {
                    Text("Back")
                }
                .padding()
                PushView(destination: DestinationView()) {
                    Text("Next")
                }
                .padding()
                .onNavigationComplete(id) {
                    print("View \(self.id)")
                }
            }
        }
    }

    struct OriginView: View {
        var id = UUID()

        var body: some View {
            PushView(destination: DestinationView()) {
                Text("Next")
            }
            .padding()
            .onNavigationComplete(id) {
                print("View \(self.id)")
            }
        }
    }

    struct Transition_Previews: PreviewProvider {
        static var previews: some View {
            VStack {
                NavigationStackView(transitionType: .default,
                                    easing: Animation.linear(duration: 1)) {
                    OriginView()
                }.environmentObject(NavigationContext())
            }
        }
    }

@spifd
Copy link
Author

spifd commented Jun 4, 2020

Hi,

I've updated my example so no UUID has to be declared in the views that want to react on the end of navigation. The idea is simply to use onNavigationComplete instead of onAppear. I still don't know whether there can be a solution maintaining onAppear to avoid coding views depending on their use during navigation vs as usual.

import Combine
import NavigationStack
import SwiftUI

class NavigationViewId: ObservableObject {
    let id: UUID = UUID()
}

class NavigationContext: ObservableObject {
    @Published var originViewId: UUID? = UUID()
    // Initial value so that onNavigationComplete is called on initial screen
}

extension View {
    func onNavigationComplete(perform action: (() -> Void)? = nil) -> some View {
        self
            .modifier(NavigationTrackModifier(action: action))
            .envNavigationId()
    }

    func envNavigationId() -> some View {
        self.environmentObject(NavigationViewId())
    }

    func envNavigationContext() -> some View {
        self.environmentObject(NavigationContext())
            .environmentObject(NavigationViewId())
    }
}

struct NavigationTrackModifier: ViewModifier {
    var action: (() -> Void)?

    @EnvironmentObject var navContext: NavigationContext

    @EnvironmentObject var navId: NavigationViewId

    func body(content: Content) -> some View {
        content
            .onDisappear {
                self.navContext.originViewId = self.navId.id
            }
            .onReceive(navContext.$originViewId) { viewId in
                if viewId != nil, viewId != self.navId.id {
                    self.action?()
                    self.navContext.originViewId = nil
                }
            }
    }
}

#if DEBUG
    struct DestinationView: View {
        static var count: Int = 0

        let viewNb: Int = {
            count += 1
            return count
        }()

        var body: some View {
            VStack {
                Text("View \(self.viewNb)")
                PopView {
                    Text("Back")
                }
                .padding()
                PushView(destination: DestinationView()) {
                    Text("Next")
                }
                .padding()
            }
            .onNavigationComplete {
                print("Navigation complete for Dest view \(self.viewNb)")
            }
        }
    }

    struct OriginView: View {
        var body: some View {
            PushView(destination: DestinationView()) {
                Text("Next")
            }
            .padding()
            .onNavigationComplete {
                print("Navigation complete for Origin view")
            }
        }
    }

    struct Navigation_Previews: PreviewProvider {
        static var previews: some View {
            VStack {
                NavigationStackView(transitionType: .default,
                                    easing: Animation.linear(duration: 1)) {
                                        OriginView()
                }.envNavigationContext()
            }
        }
    }
#endif

@xyrer
Copy link

xyrer commented Jun 30, 2020

This is particularly evident when accessing an environmentObject. Like so: https://github.com/xyrer/navigation-stack-bug

Even tho your solution might look like it works, it only does it with very simple instructions, doing anything beyond that will take it into an infinite loop and crash, best of cases it just bounces back like when using onAppear. I'm still looking into it

@matteopuc
Copy link
Owner

Hi @spifd, thank you so much for investigating this issue. Unfortunately we can't rely on the framework onAppear/onDisappear since the onAppear is called as soon as a single pixel of the new view is presented. Your solution it's the best we can do at the moment, we could even think to integrate it directly inside the NavigationStack.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants