简体   繁体   中英

SwiftUI Unexpectedly NavigationLink pops automatically

I have a simple use case where a screen pushes another screen using the NavigationLink. There is a strange behaviour iOS 14.5 where the pushed screen is popped just after being pushed.

Code:

NavigationLink(destination: EmptyView()) { EmptyView()} 

I manage to create a sample app where I reproduce it. I believe the cause is the presence of @Environment(\.presentationMode) that seem to re-create the view and it causes the pushed view to be popped.

The exact same code works fine in Xcode 12 / iOS 14.4

I was stuck in this since last week. To fix, I simply added this to my view containing my existing NavigationLink s:

NavigationLink(destination: EmptyView()) {
    EmptyView()
}

I could never find a reliable solution to this horrible bug. So I decided to create a custom NavigationLink, using Introspect ( https://github.com/siteline/SwiftUI-Introspect ). This works way better than expected, because all swiftui related functions continue working as usual. Seems like the bug is specifically with NavigationLink.

private struct NavigationLinkImpl<Destination: View, Label: View>: View {
    let destination: () -> Destination?
    @State var isActive = false
    @ViewBuilder let label: () -> Label

    var body: some View {
        NavigationLinkImpl1(destination: destination, isActive: $isActive, label: label)
    }
}

private struct NavigationLinkImpl1<Destination: View, Label: View>: View {
    let destination: () -> Destination
    @Binding var isActive: Bool
    @ViewBuilder let label: () -> Label
    @State var model = Model()

    var body: some View {
        Button(action: action, label: label)
            .introspectNavigationController(customize: handle)
            .id(isActive)
    }

    func handle(nav: UINavigationController) {
        if isActive {
            if model.destination == nil {
                let dest = UIHostingController<Destination>(rootView: destination())
                nav.pushViewController(dest, animated: true)
                model.destination = dest
            }
        } else {
            if let dest = model.destination {
                if let i = nav.viewControllers.lastIndex(of: dest) {
                    nav.setViewControllers(.init(nav.viewControllers.prefix(i + 1)), animated: true)
                }
                model.destination = nil
            }
        }
        if isActive != model.contains(nav: nav) { // detect pop
            isActive = model.contains(nav: nav)
        }
    }

    final class Model {
        var destination: UIHostingController<Destination>?
        func contains(nav: UINavigationController) -> Bool { destination.map { nav.viewControllers.contains($0) } ?? false }
    }

    func action() { isActive = true }
}

extension NavigationLink {
    init<Destination: View, Label: View>(destination: @autoclosure @escaping () -> Destination, @ViewBuilder label: @escaping () -> Label) {
        self.init(body: NavigationLinkImpl(destination: destination, label: label))
    }

    init<Destination: View, Label: View>(destination: @autoclosure @escaping () -> Destination, isActive: Binding<Bool>, @ViewBuilder label: @escaping () -> Label) {
        self.init(body: NavigationLinkImpl1(destination: destination, isActive: isActive, label: label))
    }

    init<Destination: View>(_ text: String, destination: @autoclosure @escaping () -> Destination, isActive: Binding<Bool>) {
        self.init(destination: destination(), isActive: isActive) { Text(text) }
    }

    init<Destination: View>(_ text: String, destination: @autoclosure @escaping () -> Destination) {
        self.init(destination: destination()) { Text(text) }
    }
}

Put this in a file, and your existing NavigationLinks will work just fine. Tested in ios 14 and 15

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM