How to access UIHostingController from a SwiftUI View


Greetings, traveler!

When working with SwiftUI, you often need to bridge UIKit and SwiftUI components. One common requirement is to access the underlying UIHostingController from within a SwiftUI view. This can be useful when you need to perform UIKit-specific actions such as navigation, presenting alerts, or interacting with UIKit lifecycle events.

In this article, we’ll explore a clean way to expose a UIHostingController to SwiftUI views.

Introducing HostingControllerProvider

To enable SwiftUI views to access their hosting controller, we define a provider class marked with @Observable. This class will store a weak reference to the controller:

@Observable
final class HostingControllerProvider {
    weak var viewController: UIViewController?
}

Injecting the Provider

We can create the provider and inject it into the SwiftUI view’s environment before wrapping the view into a UIHostingController. Once the controller is initialized, we assign it back to the provider:

class Router {
    var navigationController: UINavigationController?

    func show() {
        let provider = HostingControllerProvider()
        let view = SomeView().environment(provider)
        let controller = UIHostingController(rootView: view)
        provider.viewController = controller

        navigationController?.pushViewController(controller, animated: true)
    }
}

Now, any SwiftUI view within this hierarchy can access its UIHostingController through the HostingControllerProvider.

struct SomeView: View {
    @Environment(HostingControllerProvider.self) private var hostingProvider
    
    var body: some View {
        Text("Hello!")
            .onAppear {
                hostingProvider.viewController?.navigationItem.title = "Some Screen"
            }
    }
}

Adding Convenience with View Extension

To streamline this pattern, we can add an extension to View that packages the provider setup and controller initialization:

extension View {
    func hosted() -> UIHostingController<some View> {
        let provider = HostingControllerProvider()
        let view = environment(provider)
        let controller = UIHostingController(rootView: view)
        provider.viewController = controller
        return controller
    }
}

This lets you easily wrap any SwiftUI view and retrieve its hosting controller:

class Router {
    var navigationController: UINavigationController?

    func show() {
        navigationController?.pushViewController(SomeView().hosted(), animated: true)
    }
}

Summary

This approach allows SwiftUI views to interact with their hosting controllers without tightly coupling them to UIKit logic.