nathantannar4 / Transmission

Bridges UIKit presentation APIs to a SwiftUI API so you can use presentation controllers, interactive transitions and more.
BSD 2-Clause "Simplified" License
378 stars 13 forks source link

Destination link ignores pop by binding #24

Closed PhilipDukhov closed 8 months ago

PhilipDukhov commented 8 months ago

Steps to reproduce:

  1. Press Push button
  2. Press Pop button

Expected result: view will pop to root. Actual result: nothing happens.

struct ContentView: View {
    @State var pushed = false

    var body: some View {
        NavigationContainer {
            Button("Push") {
                pushed = true
            }
            .destination(isPresented: $pushed) {
                Button("Pop") {
                    pushed = false
                }
            }
        }
    }
}

struct NavigationContainer<Content: View>: UIViewControllerRepresentable {
    @ViewBuilder let content: () -> Content

    func makeUIViewController(context: Context) -> UINavigationController {
        UINavigationController(rootViewController: UIHostingController(rootView: content()))
    }

    func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {
        (uiViewController.viewControllers.first as? UIHostingController<Content>)?.rootView = content()
    }
}
PhilipDukhov commented 8 months ago

update: looks like changing state declared outside of .destination doesn't trigger recomposition when modified from inside destination.

nathantannar4 commented 8 months ago

Yea this is a known issue. I believe it's because the UIHostingController that pushed the destination view is no longer in the view hierarchy so SwiftUI updates no longer take place.

The expected workaround is to use the @Environment(\.destinationCoordinator) to pop the view programatically. I havent found a good fix to allow mutating a @State in a previous hosting controller like your example shows.

nathantannar4 commented 8 months ago

Ok I actually stumbled upon a nice fix. In 1.1.1, wrap your NavigationContainer like so:

struct NavigationContainer<Content: View>: View {
        @ViewBuilder let content: Content

        var body: some View {
            ViewGraphBridgeAdapter {
                content
            } content: { content in
                Wrapper(content: content)
            }
        }

        struct Wrapper<C: View>: UIViewControllerRepresentable {
            let content: C

            func makeUIViewController(context: Context) -> UINavigationController { }

            func updateUIViewController(_ uiViewController: UINavigationController, context: Context) { }
        }
    }