Files
iOS/Sources/App/Container/ContainerViewModel.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

98 lines
4.3 KiB
Swift

import Combine
import Shared
/// Drives the content presented over the web frontend launch messages (What's-New, then TestFlight),
/// Settings, the download manager (sheets), and the forced onboarding-permissions decision (full-screen
/// cover). Owns the launch queue and publishes the currently-presented content for `ContainerView`.
@MainActor
final class ContainerViewModel: ObservableObject {
/// A sheet presented over the web view. A single `.sheet(item:)` switches on this, so only one can be
/// presented at a time regardless of how many qualify.
enum PresentedSheet: Identifiable {
case whatsNew(WhatsNewRelease)
case testFlight(TestFlightMessage)
case settings
case assistSettings
case downloadManager(DownloadManagerViewModel)
case serverSelect(prompt: String?, includeSettings: Bool, onSelect: (Server) -> Void)
var id: String {
switch self {
case let .whatsNew(release): return "whatsNew-\(release.id)"
case let .testFlight(message): return "testFlight-\(message.id.rawValue)"
case .settings: return "settings"
case .assistSettings: return "assistSettings"
case .downloadManager: return "downloadManager"
case .serverSelect: return "serverSelect"
}
}
}
/// A forced, full-screen flow presented over the web view via `.fullScreenCover` (no swipe-to-dismiss).
enum FullScreenCover: Identifiable {
case onboardingPermissions(server: Server, steps: [OnboardingPermissionsNavigationViewModel.StepID])
var id: String {
switch self {
case .onboardingPermissions: return "onboardingPermissions"
}
}
}
@Published var presentedSheet: PresentedSheet?
@Published var fullScreenCover: FullScreenCover?
private var pendingLaunchMessages: [PresentedSheet] = []
private var didEvaluateLaunchMessages = false
/// Queues the launch messages (What's-New, then TestFlight) to present the first time the web view appears
/// the first thing the user sees via SwiftUI rather than a UIKit overlay.
func presentLaunchMessagesIfNeeded(isShowingWebView: Bool) {
guard !didEvaluateLaunchMessages, isShowingWebView else { return }
didEvaluateLaunchMessages = true
var queue: [PresentedSheet] = []
if let release = WhatsNewEngine().releaseToShow() {
queue.append(.whatsNew(release))
}
if let message = TestFlightCommunicationEngine().messageToShow() {
queue.append(.testFlight(message))
}
pendingLaunchMessages = queue
showNextLaunchMessage()
}
/// Presents the next queued launch message, if any. Called on first evaluation and on each sheet dismiss so
/// a single `.sheet` shows them in sequence only one is ever bound, avoiding competing-binding races.
func showNextLaunchMessage() {
guard presentedSheet == nil, !pendingLaunchMessages.isEmpty else { return }
presentedSheet = pendingLaunchMessages.removeFirst()
}
/// Presents app Settings as a sheet over the web view (user-triggered, e.g. a gesture or the re-auth gear).
func presentSettings() {
presentedSheet = .settings
}
/// Presents Assist settings as a sheet over the web view (triggered by the frontend's external bus).
func presentAssistSettings() {
presentedSheet = .assistSettings
}
/// Presents the download manager as a sheet (iOS 17+, a download began). The view model must be the same
/// instance the web view set as the `WKDownload` delegate.
func presentDownloadManager(_ viewModel: DownloadManagerViewModel) {
presentedSheet = .downloadManager(viewModel)
}
/// Presents the forced onboarding-permissions decision as a full-screen cover.
func presentOnboardingPermissions(server: Server, steps: [OnboardingPermissionsNavigationViewModel.StepID]) {
fullScreenCover = .onboardingPermissions(server: server, steps: steps)
}
/// Presents the server picker as a sheet (e.g. a server-less deep link, or the "show servers" gesture).
func presentServerSelect(prompt: String?, includeSettings: Bool, onSelect: @escaping (Server) -> Void) {
presentedSheet = .serverSelect(prompt: prompt, includeSettings: includeSettings, onSelect: onSelect)
}
}