iosswiftswiftui

How to navigate from UIViewController -> SwiftUI View -> SwiftUI View -> UIViewController?


Screen 1 in UIViewController

import UIKit
import SwiftUI

class UIKitScreen1ViewController: UIViewController {
    
    var userData: UserData? // Data to be passed to next screen
    
    private let nameTextField = UITextField()
    private let emailTextField = UITextField()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // UI setup (name and email text fields)
        nameTextField.placeholder = "Enter Name"
        nameTextField.frame = CGRect(x: 50, y: 100, width: 300, height: 40)
        emailTextField.placeholder = "Enter Email"
        emailTextField.frame = CGRect(x: 50, y: 150, width: 300, height: 40)
        
        let nextButton = UIButton(type: .system)
        nextButton.setTitle("Next", for: .normal)
        nextButton.frame = CGRect(x: 50, y: 200, width: 100, height: 40)
        nextButton.addTarget(self, action: #selector(nextButtonTapped), for: .touchUpInside)
        view.backgroundColor = .white
        view.addSubview(nameTextField)
        view.addSubview(emailTextField)
        view.addSubview(nextButton)
    }
    
    @objc private func nextButtonTapped() {
        // Create a UserData object
        userData = UserData(name: nameTextField.text ?? "", email: emailTextField.text ?? "")
        
        // Navigate to Screen 2 (SwiftUI)
        let screen2 = UIHostingController(rootView: SwiftUIScreen2(userData: userData!))
        navigationController?.pushViewController(screen2, animated: true)
    }
}

struct UserData {
    let name: String
    let email: String
}


Screen 2 in SwiftUI

struct SwiftUIScreen2: View {
    @State var userData: UserData
    @State private var isNextScreenActive = false
    @State private var isUIKitNavbarHidden = false
    
    var body: some View {
        NavigationStack {
            VStack {
                Text("Name: \(userData.name)")
                Text("Email: \(userData.email)")
                
                Button("Go to Screen 3") {
                    isNextScreenActive = true
                }
                .padding()
                // Navigate to Screen 3
            }
            .navigationTitle("Screen 2")
            .navigationBarTitleDisplayMode(.inline)
            .navigationDestination(
                 isPresented: $isNextScreenActive) {
                     SwiftUIScreen3(userData: userData)
                      Text("")
                          .hidden()
                 }
        }
    }
}

Screen 3 in SwiftUI

struct SwiftUIScreen3: View {
    @State var userData: UserData
    @State private var isNextScreenActive = false
    
    var body: some View {
        NavigationStack {
            VStack {
                Text("Name: \(userData.name)")
                Text("Email: \(userData.email)")
                
                Button("Go to Screen 4") {
                    isNextScreenActive = true
                }
                .padding()
            }
            .navigationTitle("Screen 3")
            .navigationBarTitleDisplayMode(.inline)
            .navigationDestination(
                 isPresented: $isNextScreenActive) {
                     UIKitScreen4ViewControllerWrapper(userData: userData)
                      Text("")
                          .hidden()
                 }
        }
    }
}

Screen 4 in UIViewController

struct UIKitScreen4ViewControllerWrapper: UIViewControllerRepresentable {
    var userData: UserData

    func makeUIViewController(context: Context) -> UIKitScreen4ViewController {
        return UIKitScreen4ViewController(userData: userData)
    }

    func updateUIViewController(_ uiViewController: UIKitScreen4ViewController, context: Context) {
        // You can update the view controller here if needed (not necessary in this case)
    }
}

class UIKitScreen4ViewController: UIViewController {
    
    var userData: UserData
    
    private let nameLabel = UILabel()
    private let emailLabel = UILabel()
    
    init(userData: UserData) {
        self.userData = userData
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // UI setup (name and email labels)
        nameLabel.text = "Name: \(userData.name)"
        nameLabel.frame = CGRect(x: 50, y: 100, width: 300, height: 40)
        emailLabel.text = "Email: \(userData.email)"
        emailLabel.frame = CGRect(x: 50, y: 150, width: 300, height: 40)
        
        view.addSubview(nameLabel)
        view.addSubview(emailLabel)
    }
}

I am facing double navigation bar issue where I can see UIKit's nav bar and SwiftUI's nav bar. I have looked everywhere online and I can't find solution for this issue due to navigationStack in swiftUI Double navigation bar issue

I also face issue with transition from screen 3(SwiftUI) to screen 4(UIViewController)

I can not figure out a way to make this flow work. I would really appreciate it if anyone can help me out.

I have tried hiding the Navigation bar when pushing the the swiftui hosting controller but navigation flow breaks when I reach the Screen4. Ultimately decided to post here for help


Solution

  • Since you're mixing UIKit and SwiftUI and trying to navigate between them, you need to remove NavigationStack because UIKitScreen1ViewController also has a navigationController. That's why it displays twice.

    struct SwiftUIScreen2: View {
        var body: some View {
            NavigationStack { //<- need to be removed
                ...
            }
        }
    }
    

    navigationDestination only works within NavigationStack, thus replace it with NavigationLink like below:

    struct SwiftUIScreen2: View {
        ...
        VStack {
            Text("Name: \(userData.name)")
            Text("Email: \(userData.email)")
            NavigationLink(destination: SwiftUIScreen3(userData: userData)) {
                Text("Go to screen 3")
            }
            .padding()
        }
        ...
    }
    

    Do it the same with SwiftUIScreen3. This is the output:

    enter image description here


    Another approach is wrapping all controllers into UIViewControllerRepresentable while holding a navigationPath. And you will have a single NavigationStack that navigates the flow through paths, as @workingdog mentioned.