Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Examples/Sources/WindowingExample/WindowingApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ struct AlertDemo: View {

Button("Present error") {
Task {
await presentAlert("Failed to succeed")
await presentAlert("Failed to succeed") {}
}
}
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -190,6 +191,9 @@ struct WindowingApp: App {
#endif

AlertDemo()
Button("Show alert scene") {
isAlertSceneShown = true
}

Divider()

Expand All @@ -213,6 +217,9 @@ struct WindowingApp: App {
}
}
}

AlertScene("Alert scene", isPresented: $isAlertSceneShown) {}

#if !(os(iOS) || os(tvOS) || os(Windows))
WindowGroup("Secondary window") {
#hotReloadable {
Expand Down
5 changes: 5 additions & 0 deletions Sources/SwiftCrossUI/Builders/AlertActionsBuilder.swift
Original file line number Diff line number Diff line change
@@ -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(
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftCrossUI/Environment/Actions/AlertAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
81 changes: 81 additions & 0 deletions Sources/SwiftCrossUI/Scenes/AlertScene.swift
Original file line number Diff line number Diff line change
@@ -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<Bool>,
@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<Backend: AppBackend>(
from scene: AlertScene,
backend: Backend,
environment: EnvironmentValues
) {
self.scene = scene
}

public func update<Backend: AppBackend>(
_ 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
}
}
}
9 changes: 7 additions & 2 deletions Sources/UIKitBackend/UIKitBackend+Window.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,16 @@ extension UIKitBackend {
public typealias Window = UIWindow

public func createWindow(withDefaultSize _: SIMD2<Int>?) -> 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()
}
Expand Down
Loading