Files
iOS/Sources/App/Frontend/WebView/WebViewController+EmptyState.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

102 lines
3.7 KiB
Swift

import Shared
import SwiftUI
import UIKit
// MARK: - Empty State
extension WebViewController {
func emptyStateStyle(for connectionState: FrontEndConnectionState) -> WebViewEmptyStateStyle {
switch connectionState {
case .authInvalid:
.unauthenticated
case .connected, .disconnected, .unknown:
.disconnected
}
}
/// Shows the disconnected/unauthenticated empty state as a SwiftUI overlay in `HomeAssistantView` (via
/// `overlayState`) rather than an alpha-animated subview, so app-level sheets can float over it.
func showEmptyState() {
overlayState?.emptyState = makeEmptyStateContent()
}
@objc func hideEmptyState() {
overlayState?.emptyState = nil
}
var shouldShowErrorDetailsButton: Bool {
connectionState == .disconnected && latestLoadError != nil
}
func presentLatestLoadErrorDetails() {
guard let latestLoadError else { return }
presentOverlayController(
controller: UIHostingController(rootView: ConnectionErrorDetailsView(
server: server,
error: latestLoadError
)),
animated: true
)
}
// To avoid keeping the empty state on screen when user is disconnected in background
// due to inactivity, we reset the empty state timer
@objc func resetEmptyStateTimerWithLatestConnectedState() {
let state: FrontEndConnectionState = if connectionState == .authInvalid {
.authInvalid
} else {
isConnected ? .connected : .disconnected
}
updateFrontendConnectionState(state: state.rawValue)
}
func emptyStateObservations() {
// Hide empty state when enter background
NotificationCenter.default.addObserver(
self,
selector: #selector(hideEmptyState),
name: UIApplication.didEnterBackgroundNotification,
object: nil
)
// Show empty state again if after entering foreground it is not connected
NotificationCenter.default.addObserver(
self,
selector: #selector(resetEmptyStateTimerWithLatestConnectedState),
name: UIApplication.willEnterForegroundNotification,
object: nil
)
}
func removeEmptyStateObservations() {
NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.removeObserver(
self,
name: UIApplication.willEnterForegroundNotification,
object: nil
)
}
private func makeEmptyStateContent() -> WebFrontendOverlayState.EmptyStateContent {
WebFrontendOverlayState.EmptyStateContent(
style: emptyStateStyle(for: connectionState),
server: server,
showsErrorDetailsButton: shouldShowErrorDetailsButton,
availableReauthURLTypes: availableReauthURLTypes(for: server),
retryAction: { [weak self] in
self?.hideEmptyState()
self?.refresh()
},
settingsAction: { [weak self] in self?.showSettingsViewController() },
errorDetailsAction: { [weak self] in self?.presentLatestLoadErrorDetails() },
reauthAction: { [weak self] urlType in self?.performReauthentication(using: urlType) },
dismissAction: { [weak self] in self?.hideEmptyState() }
)
}
/// Available URL types for re-authentication, ordered by preference: remote UI > external > internal.
private func availableReauthURLTypes(for server: Server) -> [ConnectionInfo.URLType] {
[.remoteUI, .external, .internal].filter { server.info.connection.address(for: $0) != nil }
}
}