Search code examples
swiftuiuikitswiftui-navigationlinkuiviewcontrollerrepresentable

Go Back to SwiftUi View from UIViewControllerRepresentable


I want to return to the previous view via a UIViewController button in a UIViewControllerRepresentable.

When I click on uiButtonBack, nothing happens.

What is wrong in my code?

MainContent:

struct ContentView: View {
    var body: some View {
         NavigationView {
            VStack {
                NavigationLink(destination: MyUIKitView()) { Text("Go to UIKit View") }
            }
            .navigationTitle("Main View")
        }
    }
}

UIViewControllerRepresentable:

struct MyUIKitView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> MyUiViewController {
        let viewController = MyUiViewController()
        return viewController
    }

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

UIViewController:

class MyUiViewController: UIViewController {

    
    override func viewDidLoad() {
        super.viewDidLoad()
        mainView.uiButtonBack.addTarget(self,
                                           action: #selector(back),
                                           for: .touchUpInside)
        mainView.uiButtonBack.setTitle("Back", for: .normal)
    }

    @objc func back() {
        self.navigationController?.popToRootViewController(animated: true)
    }
}

Thanks to help me.


Solution

  • By inspecting the view controller hierarchy, it seems like SwiftUI wraps the content of the NavigationView into some sort split view controller by default.

    If you set the style of the NavigationView to .stack, your code works correctly.

    NavigationView {
        ...
    }
    .navigationViewStyle(.stack)
    

    If you don't need to support versions before iOS 16, you should use NavigationStack instead, which also makes popToRootViewController work correctly.

    NavigationStack {
        VStack {
            NavigationLink {
                MyUIKitView()
            } label: {
                Text("Go to UIKit View")
            }
        }
        .navigationTitle("Main View")
    }
    

    That said, using popToRootViewController depends on the fact that SwiftUI wraps your UIKit view controller in a UINavigationController, which might change in the future. IMO, it is better to just pass the dismiss action from the SwiftUI side to the UIKit side:

    struct MyUIKitView: UIViewControllerRepresentable {
        @Environment(\.dismiss) var dismiss
        
        func makeUIViewController(context: Context) -> MyUiViewController {
            let viewController = MyUiViewController()
            
            return viewController
        }
        
        func updateUIViewController(_ uiViewController: MyUiViewController, context: Context) { 
            uiViewController.dismissAction = dismiss
        }
    }
    
    class MyUiViewController: UIViewController {
        
        var dismissAction: DismissAction?
        
        override func loadView() {
            // ...
        }
    
        @objc func back() {
            dismissAction?()
        }
    }