Files
iOS/Sources/Shared/Common/Extensions/View+HA.swift
Bruno Pantaleão Gonçalves 6e84ff4cb6 Migrate app from UIKit based app to SwiftUI (#4748)
<!-- Thank you for submitting a Pull Request and helping to improve Home
Assistant. Please complete the following sections to help the processing
and review of your changes. Please do not delete anything from this
template. -->

## Summary
<!-- Provide a brief summary of the changes you have made and most
importantly what they aim to achieve -->
This PR is a massive refactor of how the app handles UI presentation and
navigation, goin from the UIKit based apps style to SwiftUI.

## Screenshots
<!-- If this is a user-facing change not in the frontend, please include
screenshots in light and dark mode. -->

## Link to pull request in Documentation repository
<!-- Pull requests that add, change or remove functionality must have a
corresponding pull request in the Companion App Documentation repository
(https://github.com/home-assistant/companion.home-assistant). Please add
the number of this pull request after the "#" -->
Documentation: home-assistant/companion.home-assistant#

## Any other notes
<!-- If there is any other information of note, like if this Pull
Request is part of a bigger change, please include it here. -->

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 10:33:17 +02:00

92 lines
3.3 KiB
Swift

import Foundation
import SwiftUI
public extension View {
func embeddedInHostingController() -> UIHostingController<some View> {
let provider = ViewControllerProvider()
let hostingAccessingView = environmentObject(provider)
let hostingController = UIHostingController(rootView: hostingAccessingView)
provider.viewController = hostingController
return hostingController
}
}
public final class ViewControllerProvider: ObservableObject {
public fileprivate(set) weak var viewController: UIViewController?
}
// MARK: - UIViewController in SwiftUI
public struct ViewControllerWrapper<T: UIViewController>: UIViewControllerRepresentable {
private let viewController: T
private let configure: ((T) -> Void)?
public init(_ viewController: T, configure: ((T) -> Void)? = nil) {
self.viewController = viewController
self.configure = configure
}
public func makeUIViewController(context: Context) -> T {
configure?(viewController)
return viewController
}
public func updateUIViewController(_ uiViewController: T, context: Context) {
// Update the view controller if needed
configure?(uiViewController)
}
}
public extension View {
func embed<T: UIViewController>(_ viewController: T, configure: ((T) -> Void)? = nil) -> some View {
ViewControllerWrapper(viewController, configure: configure)
}
}
// MARK: - ViewControllerProvider for SwiftUI-presented views
public extension View {
/// Injects a `ViewControllerProvider` whose `viewController` resolves to the UIKit controller hosting this
/// view, for SwiftUI-presented contexts (e.g. a `.sheet`) that render a provider-dependent view directly
/// rather than through `embeddedInHostingController()`. The view keeps SwiftUI's own `\.dismiss` working
/// while still getting a presenter for UIKit modals / the in-app browser.
func injectingViewControllerProvider() -> some View {
modifier(InjectViewControllerProvider())
}
}
private struct InjectViewControllerProvider: ViewModifier {
@StateObject private var provider = ViewControllerProvider()
func body(content: Content) -> some View {
content
.environmentObject(provider)
.background(ViewControllerResolver { provider.viewController = $0 })
}
}
/// Reports the UIKit view controller hosting it so a sibling SwiftUI view can use it as a presenter.
private struct ViewControllerResolver: UIViewControllerRepresentable {
let onResolve: (UIViewController) -> Void
func makeUIViewController(context: Context) -> ResolverViewController {
let controller = ResolverViewController()
controller.onResolve = onResolve
return controller
}
func updateUIViewController(_ uiViewController: ResolverViewController, context: Context) {}
}
private final class ResolverViewController: UIViewController {
var onResolve: ((UIViewController) -> Void)?
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
// Our parent is the controller hosting the SwiftUI presentation (e.g. the sheet); it can present
// UIKit modals and serves as the in-app browser sender.
guard let parent else { return }
onResolve?(parent)
}
}