mirror of
https://github.com/home-assistant/iOS.git
synced 2026-04-12 14:30:51 -05:00
<!-- 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 --> ## Screenshots <!-- If this is a user-facing change not in the frontend, please include screenshots in light and dark mode. --> <img width="1796" height="804" alt="CleanShot 2026-01-16 at 13 11 31@2x" src="https://github.com/user-attachments/assets/4c0be252-f3b6-4ff5-b27e-8b11f19eaee4" /> ## 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. -->
148 lines
5.2 KiB
Swift
148 lines
5.2 KiB
Swift
import SFSafeSymbols
|
|
import SwiftUI
|
|
|
|
/// A manager class that provides imperative control over toast presentation.
|
|
///
|
|
/// Use this class to show and hide toasts from UIKit contexts where SwiftUI view modifiers
|
|
/// are not applicable. The manager maintains a shared overlay window that displays toasts
|
|
/// using the Dynamic Island-style animation.
|
|
///
|
|
/// Example usage:
|
|
/// ```swift
|
|
/// if #available(iOS 18, *) {
|
|
/// ToastManager.shared.show(
|
|
/// id: "my-toast",
|
|
/// symbol: .checkmarkSealFill,
|
|
/// symbolForegroundStyle: (.white, .green),
|
|
/// title: "Success",
|
|
/// message: "Operation completed"
|
|
/// )
|
|
///
|
|
/// // Later, to hide:
|
|
/// ToastManager.shared.hide(id: "my-toast")
|
|
/// }
|
|
/// ```
|
|
@available(iOS 18, *)
|
|
@MainActor
|
|
public final class ToastManager {
|
|
/// The shared singleton instance of the toast manager.
|
|
public static let shared = ToastManager()
|
|
|
|
public static var toastComponentVersion = 1
|
|
private var overlayWindow: PassThroughWindow?
|
|
private var overlayController: ToastHostingController?
|
|
private var autoDismissTask: Task<Void, Never>?
|
|
public init() {}
|
|
|
|
/// Shows a toast with the specified parameters.
|
|
///
|
|
/// - Parameters:
|
|
/// - id: A unique identifier for the toast. Used to hide a specific toast later.
|
|
/// - symbol: The SF Symbol to display in the toast.
|
|
/// - symbolFont: The font for the symbol. Defaults to system size 35.
|
|
/// - symbolForegroundStyle: A tuple of colors for the symbol's primary and secondary styles.
|
|
/// - title: The title text displayed in the toast.
|
|
/// - message: The message text displayed below the title.
|
|
/// - duration: Optional duration in seconds after which the toast auto-dismisses.
|
|
/// Pass `nil` for a permanent toast that must be dismissed manually.
|
|
public func show(
|
|
id: String,
|
|
symbol: String,
|
|
symbolFont: Font = .system(size: 35),
|
|
symbolForegroundStyle: (Color, Color),
|
|
title: String,
|
|
message: String? = nil,
|
|
duration: TimeInterval? = nil
|
|
) {
|
|
let toast = Toast(
|
|
id: id,
|
|
symbol: symbol,
|
|
symbolFont: symbolFont,
|
|
symbolForegroundStyle: symbolForegroundStyle,
|
|
title: title,
|
|
message: message ?? ""
|
|
)
|
|
show(toast: toast, duration: duration)
|
|
}
|
|
|
|
/// Shows the specified toast.
|
|
///
|
|
/// - Parameters:
|
|
/// - toast: The toast to display.
|
|
/// - duration: Optional duration in seconds after which the toast auto-dismisses.
|
|
/// Pass `nil` for a permanent toast that must be dismissed manually.
|
|
public func show(toast: Toast, duration: TimeInterval? = nil) {
|
|
// Cancel any pending auto-dismiss
|
|
autoDismissTask?.cancel()
|
|
autoDismissTask = nil
|
|
|
|
// Create or get the overlay window
|
|
ensureOverlayWindow()
|
|
|
|
guard let overlayWindow else { return }
|
|
|
|
// Set the toast and present it
|
|
overlayWindow.toast = toast
|
|
overlayWindow.isPresented = true
|
|
overlayController?.isStatusBarHidden = true
|
|
|
|
// Schedule auto-dismiss if duration is provided
|
|
if let duration {
|
|
autoDismissTask = Task { [weak self] in
|
|
try? await Task.sleep(for: .seconds(duration))
|
|
guard !Task.isCancelled else { return }
|
|
self?.hide(id: toast.id)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Hides the toast with the specified ID.
|
|
///
|
|
/// - Parameter id: The identifier of the toast to hide.
|
|
public func hide(id: String) {
|
|
guard let overlayWindow, overlayWindow.toast?.id == id else { return }
|
|
hideCurrentToast()
|
|
}
|
|
|
|
/// Hides any currently displayed toast.
|
|
public func hideCurrentToast() {
|
|
autoDismissTask?.cancel()
|
|
autoDismissTask = nil
|
|
|
|
overlayWindow?.isPresented = false
|
|
overlayController?.isStatusBarHidden = false
|
|
}
|
|
|
|
// MARK: - Private Methods
|
|
|
|
private func ensureOverlayWindow() {
|
|
#if os(iOS)
|
|
// Try to find an existing window first
|
|
if let windowScene = Current.application?().connectedScenes
|
|
.compactMap({ $0 as? UIWindowScene })
|
|
.first(where: { $0.activationState == .foregroundActive }) {
|
|
if let existingWindow = windowScene.windows.first(where: { $0.tag == 1009 }) as? PassThroughWindow {
|
|
overlayWindow = existingWindow
|
|
overlayController = existingWindow.rootViewController as? ToastHostingController
|
|
return
|
|
}
|
|
|
|
// Create a new overlay window
|
|
let window = PassThroughWindow(windowScene: windowScene)
|
|
window.backgroundColor = .clear
|
|
window.isHidden = false
|
|
window.isUserInteractionEnabled = true
|
|
window.tag = 1009
|
|
window.windowLevel = .statusBar + 1
|
|
|
|
let hostingController = ToastHostingController(rootView: ToastView(window: window))
|
|
hostingController.view.backgroundColor = .clear
|
|
window.rootViewController = hostingController
|
|
|
|
overlayWindow = window
|
|
overlayController = hostingController
|
|
}
|
|
#endif
|
|
}
|
|
}
|