1

My app is a hybrid of UIKit & SwiftUI. I have a use case where a user may need to tap on a Button from a SwiftUI.View but push to a UIViewController or another UIHostingController.

My project uses Storyboard.

I'm using UINavigationController & UITabBarController.

There are two scenarios I am looking at.

1.) From my initial launch on my home screen, I can tap a button and within its action I have:

let vc = UIHostingController(rootView: RootView())
self.navigationController?.pushViewController(vc, animated: true)

This works as expected.

2.) I tap on a different tab and it defaults to my SwiftUI RootView which is hosted in a custom UIHostingController which I use in my Storyboard. Here, if I tap on a button to trigger the push, it doesn't push. I just see the View I am on update.

I'm also using a custom UINavigationController. From my Storyboard, the tabs relationship goes to the custom UINavigationController & then its root is the appropriate UIViewController. In one scenario though it's my custom UIHostingController so I can load a SwiftUI View initially from the tab selection.

Here is what I have tried doing to handle push to a View Controller from my SwiftUI View:

final class AppData: ObservableObject {
    weak var window: UIWindow? // Will be nil in SwiftUI previewers

    init(window: UIWindow? = nil) {
        self.window = window
    }
    
    public func pushViewController(_ viewController: UIViewController, animated: Bool = true) {
        let nvc = window?.rootViewController?.children.first?.children.first as? UINavigationController
        nvc?.pushViewController(viewController, animated: animated)
    }
}

// This is what is triggered from the Button action.
func optionSelected(option: String) {
    if let optionId = Common.getIDByOption(option: option) {
        let vc = UIHostingController(rootView: RootView())                
        appData.pushViewController(vc, animated: true)
    }
}

What happens:

  • I do see the data change, but it's all on the same View I am already on.
  • I need to push to the new UIHostingController.
Luke Irvin
  • 1,179
  • 1
  • 20
  • 39
  • Could you provide more context? Does this views are controlled with `UINavigationController` or `NavigationLink`? – Błażej Jul 10 '21 at 06:58
  • @Błażej I have added more context above. Please let me know if any additional context is needed. – Luke Irvin Jul 11 '21 at 16:19

1 Answers1

0

If you're mixing UIKit and SwiftUI in a way where UITabBarController and UINavigationController are handling the navigation. I advise you to cut NavigationView and NavigationLink. The reason behind it is simple. SwiftUI.View will be recreated on each switch to tab. Hence, you would start from the begining. You could walk around it, but in this situation, easier will be use UINavigationController.

Let's assume you're app has two tabs. I would put SwiftUI.View in UIHostingController and then in UINavigationController. In SwiftUI.View I would put closure let onTap: () -> Void which would be called whenever you need to push next UIHostingController or UIViewController to UINavigationController.

Example

//: A UIKit based Playground for presenting user interface

import UIKit
import SwiftUI
import PlaygroundSupport

final class MainViewController: UITabBarController {
    let firstTab: UINavigationController = {
        let navigationController = UINavigationController()
        navigationController.tabBarItem = UITabBarItem(title: "First", image: nil, tag: 1)
        return navigationController
    }()

    let secondTab: UINavigationController = {
        let navigationController = UINavigationController()
        navigationController.tabBarItem = UITabBarItem(title: "Second", image: nil, tag: 1)
        return navigationController
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        setUpFirstTab()
        setUpSecondTab()
        viewControllers = [firstTab, secondTab]
    }

    func setUpFirstTab() {
        let firstView = FirstView { number in
            let secondViewHostingController = UIHostingController(rootView: SecondView(number: number))
            self.firstTab.pushViewController(secondViewHostingController, animated: true)
        }
        let firstViewHostingController = UIHostingController(rootView: firstView)
        firstTab.tabBarItem = UITabBarItem(title: "First", image: nil, tag: 1)
        firstTab.viewControllers = [firstViewHostingController]
    }

    func setUpSecondTab() {
        secondTab.tabBarItem = UITabBarItem(title: "Second", image: nil, tag: 2)
        secondTab.viewControllers = [FirstViewController()]
    }
}

// Views for the first tab
struct FirstView: View {
    let onTap: (Int) -> Void
    @State var number: Int = 0
    var body: some View {
        VStack {
            Stepper("Number \(number)", value: $number)
            Button {
                onTap(number)
            } label: {
                Text("Go to the next view")
            }
        }
    }
}

struct SecondView: View {
    let number: Int
    var body: some View {
        Text("Final view with number \(number)")
    }
}

// Views controllers for the second tab
final class FirstViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let button: UIButton = UIButton(type: .roundedRect, primaryAction: UIAction(title: "show next view controller") { _ in
            self.navigationController?.pushViewController(SecondViewController(), animated: true)
        })
        button.translatesAutoresizingMaskIntoConstraints = false

        view.addSubview(button)
        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }
}

final class SecondViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemPink
    }
}
Błażej
  • 3,617
  • 7
  • 35
  • 62