Files
iOS/Sources/App/Container/ContainerView.swift
Bruno Pantaleão Gonçalves 8aa807c61c WebView wrapper improvements (#4811)
<!-- 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 -->
- Reduce UIKit usage
- Each server has it's own self-healing webview with connectivity
handling
## 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: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-23 14:39:49 +02:00

120 lines
5.8 KiB
Swift

import PromiseKit
import Shared
import SwiftUI
import UIKit
struct ContainerView: View {
@StateObject private var state = OnboardingStateObservable()
@StateObject private var viewModel = ContainerViewModel()
@State private var coordinator = AppContainerCoordinator()
var body: some View {
Group {
switch state.screen {
case let .onboarding(style):
// Host onboarding via `embeddedInHostingController()` (inside `OnboardingHostingView`): the
// flow reads an `@EnvironmentObject ViewControllerProvider` (e.g. `OnboardingServersListView`,
// to present the OAuth flow) that the SwiftUI `WindowGroup` does not inject a direct render
// would crash on first access. `.id(style)` rebuilds the controller if the style changes.
OnboardingHostingView(onboardingStyle: style)
.id(style)
case let .webView(server):
HomeAssistantView(server: server) { webViewController in
coordinator.setFrontend(webViewController)
Current.sceneManager.setWebViewController(webViewController)
}
.id(server.identifier.rawValue)
case .recoveredServerImport:
RecoveredServersImportView(onImport: { state.completeRecoveredServerImport() })
case let .recoveredServerReauth(server):
RecoveredServerReauthHostingView(server: server, state: state)
}
}
.navigationTitle(" ") // Remove default macOS title
.onAppear {
coordinator.onOpenServer = { state.showWebView(for: $0) }
coordinator.onSetup = { state.reevaluate() }
coordinator.onShowSettings = { viewModel.presentSettings() }
coordinator.onShowAssistSettings = { viewModel.presentAssistSettings() }
coordinator.onShowDownloadManager = { viewModel.presentDownloadManager($0) }
coordinator.onShowOnboardingPermissions = { viewModel.presentOnboardingPermissions(server: $0, steps: $1) }
coordinator.onSelectServer = { prompt, includeSettings in
viewModel.presentServerSelect(prompt: prompt, includeSettings: includeSettings) {
coordinator.completeServerSelection($0)
}
}
Current.sceneManager.registerAppCoordinator(coordinator)
viewModel.presentLaunchMessagesIfNeeded(isShowingWebView: isShowingWebView)
}
.onChange(of: state.screen) { _ in
viewModel.presentLaunchMessagesIfNeeded(isShowingWebView: isShowingWebView)
}
.sheet(item: $viewModel.presentedSheet, onDismiss: { viewModel.showNextLaunchMessage() }) { sheet in
switch sheet {
case let .whatsNew(release):
WhatsNewView(release: release) { WhatsNewEngine().markSeen(release) }
case let .testFlight(message):
TestFlightCommunicationView(message: message) {
TestFlightCommunicationEngine().markSeen(message)
}
case .settings:
SettingsView().injectingViewControllerProvider()
.onDisappear { refreshWebViewIfDisconnected() }
case .assistSettings:
AssistSettingsView()
case let .downloadManager(viewModel):
// The case is only ever set on iOS 17+ (the `WKDownload` delegate path); guard for the floor.
if #available(iOS 17.0, *) {
DownloadManagerView(viewModel: viewModel)
#if !targetEnvironment(macCatalyst)
.presentationDetents([.medium, .large])
#endif
}
case let .serverSelect(prompt, includeSettings, onSelect):
ServerSelectView(prompt: prompt, includeSettings: includeSettings, selectAction: onSelect)
.modify { view in
if #available(iOS 16.4, *) {
view
.presentationDetents([.medium, .large])
.presentationBackground(Color(uiColor: .systemBackground))
} else {
view
}
}
}
}
.fullScreenCover(item: $viewModel.fullScreenCover, onDismiss: { refreshWebView() }) { cover in
switch cover {
case let .onboardingPermissions(server, steps):
NavigationView {
OnboardingPermissionsNavigationView(onboardingServer: server, steps: steps)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
CloseButton { viewModel.fullScreenCover = nil }
}
}
}
.navigationViewStyle(.stack)
.injectingViewControllerProvider()
}
}
}
/// Re-evaluates the web view after a forced cover (onboarding permissions) is dismissed, mirroring the
/// old `presentOverlayController`'s `onDisappear { refresh() }`.
private func refreshWebView() {
Current.sceneManager.webViewControllerPromise.done { $0.refresh() }
}
/// Re-evaluates the web view after Settings closes, but only when it isn't connected so closing Settings
/// on a healthy page doesn't reload it, while the no-active-URL / connection block still re-evaluates.
private func refreshWebViewIfDisconnected() {
Current.sceneManager.webViewControllerPromise.done { $0.refreshIfDisconnected() }
}
private var isShowingWebView: Bool {
if case .webView = state.screen { return true }
return false
}
}