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.
If you enjoyed this article, please feel free to follow me on my social media: