diff --git a/Examples/Sources/WindowingExample/WindowingApp.swift b/Examples/Sources/WindowingExample/WindowingApp.swift index 438bd7b72d8..69de82d110b 100644 --- a/Examples/Sources/WindowingExample/WindowingApp.swift +++ b/Examples/Sources/WindowingExample/WindowingApp.swift @@ -59,7 +59,7 @@ struct AlertDemo: View { Button("Present error") { Task { - await presentAlert("Failed to succeed") + await presentAlert("Failed to succeed") {} } } } @@ -162,6 +162,7 @@ struct SheetDemo: View { struct WindowingApp: App { @State var title = "My window" @State var resizable = false + @State var isAlertSceneShown = false @State var toggle = false var body: some Scene { @@ -190,6 +191,9 @@ struct WindowingApp: App { #endif AlertDemo() + Button("Show alert scene") { + isAlertSceneShown = true + } Divider() @@ -213,6 +217,9 @@ struct WindowingApp: App { } } } + + AlertScene("Alert scene", isPresented: $isAlertSceneShown) {} + #if !(os(iOS) || os(tvOS) || os(Windows)) WindowGroup("Secondary window") { #hotReloadable { diff --git a/Sources/SwiftCrossUI/Builders/AlertActionsBuilder.swift b/Sources/SwiftCrossUI/Builders/AlertActionsBuilder.swift index a78436c86c8..b864694cee0 100644 --- a/Sources/SwiftCrossUI/Builders/AlertActionsBuilder.swift +++ b/Sources/SwiftCrossUI/Builders/AlertActionsBuilder.swift @@ -1,6 +1,11 @@ /// A builder for `[AlertAction]`. @resultBuilder public struct AlertActionsBuilder { + /// If no actions are provided, return a default "OK" action. + public static func buildBlock() -> [AlertAction] { + [.default] + } + public static func buildPartialBlock(first: Button) -> [AlertAction] { [ AlertAction( diff --git a/Sources/SwiftCrossUI/Environment/Actions/AlertAction.swift b/Sources/SwiftCrossUI/Environment/Actions/AlertAction.swift index 3ad758ddb07..e2429954857 100644 --- a/Sources/SwiftCrossUI/Environment/Actions/AlertAction.swift +++ b/Sources/SwiftCrossUI/Environment/Actions/AlertAction.swift @@ -5,7 +5,7 @@ /// breaking ``Button``'s API would have much more wide-reaching impacts than /// breaking this single-purpose API. public struct AlertAction: Sendable { - public static let ok = AlertAction(label: "Ok", action: {}) + public static let `default` = AlertAction(label: "OK", action: {}) public var label: String public var action: @MainActor @Sendable () -> Void diff --git a/Sources/SwiftCrossUI/Environment/Actions/PresentAlertAction.swift b/Sources/SwiftCrossUI/Environment/Actions/PresentAlertAction.swift index a045493766f..931aed9de87 100644 --- a/Sources/SwiftCrossUI/Environment/Actions/PresentAlertAction.swift +++ b/Sources/SwiftCrossUI/Environment/Actions/PresentAlertAction.swift @@ -10,7 +10,7 @@ public struct PresentAlertAction { @discardableResult public func callAsFunction( _ title: String, - @AlertActionsBuilder actions: () -> [AlertAction] = { [.ok] } + @AlertActionsBuilder actions: () -> [AlertAction] = { [.default] } ) async -> Int { let actions = actions() diff --git a/Sources/SwiftCrossUI/Scenes/AlertScene.swift b/Sources/SwiftCrossUI/Scenes/AlertScene.swift new file mode 100644 index 00000000000..137cf7db478 --- /dev/null +++ b/Sources/SwiftCrossUI/Scenes/AlertScene.swift @@ -0,0 +1,81 @@ +/// A scene that shows a standalone alert. +/// +/// The exact behavior of the alert is backend-dependent, but it typically +/// shows up as an application modal, or attaches itself to the app's main +/// window. +public struct AlertScene: Scene { + public typealias Node = AlertSceneNode + + var title: String + @Binding var isPresented: Bool + var actions: [AlertAction] + + public let commands = Commands.empty + + /// Creates an alert scene. + /// + /// The exact behavior of the alert is backend-dependent, but it typically + /// shows up as an application modal, or attaches itself to the app's main + /// window. + /// + /// - Parameters: + /// - title: The alert's title. + /// - isPresented: A binding to a `Bool` that controls whether the alert + /// is presented. + /// - actions: The alert's actions. + public init( + _ title: String, + isPresented: Binding, + @AlertActionsBuilder actions: () -> [AlertAction] + ) { + self.title = title + self._isPresented = isPresented + self.actions = actions() + } +} + +/// The scene graph node for ``AlertScene``. +public final class AlertSceneNode: SceneGraphNode { + public typealias NodeScene = AlertScene + + private var scene: AlertScene + private var alert: Any? + + public init( + from scene: AlertScene, + backend: Backend, + environment: EnvironmentValues + ) { + self.scene = scene + } + + public func update( + _ newScene: AlertScene?, + backend: Backend, + environment: EnvironmentValues + ) { + if let newScene { + self.scene = newScene + } + + if scene.isPresented, alert == nil { + let alert = backend.createAlert() + backend.updateAlert( + alert, + title: scene.title, + actionLabels: scene.actions.map(\.label), + environment: environment + ) + backend.showAlert(alert, window: nil) { responseId in + self.alert = nil + self.scene.isPresented = false + self.scene.actions[responseId].action() + } + + self.alert = alert + } else if !scene.isPresented, let alert { + backend.dismissAlert(alert as! Backend.Alert, window: nil) + self.alert = nil + } + } +} diff --git a/Sources/UIKitBackend/UIKitBackend+Window.swift b/Sources/UIKitBackend/UIKitBackend+Window.swift index 0aadba7bf5a..cbc6b0c8793 100644 --- a/Sources/UIKitBackend/UIKitBackend+Window.swift +++ b/Sources/UIKitBackend/UIKitBackend+Window.swift @@ -70,11 +70,16 @@ extension UIKitBackend { public typealias Window = UIWindow public func createWindow(withDefaultSize _: SIMD2?) -> Window { - var window: UIWindow + let window: UIWindow if !Self.hasReturnedAWindow { + if let mainWindow = Self.mainWindow { + window = mainWindow + } else { + window = UIWindow() + Self.mainWindow = window + } Self.hasReturnedAWindow = true - window = Self.mainWindow ?? UIWindow() } else { window = UIWindow() }