Skip to content

Commit 2644ee1

Browse files
committed
Create presentAlert env action, don't allow env action access from App
The @Environment property wrapper shouldn't allow environment actions (or at least ones that require a window handle) to be accessed from an App's top-level as the property wrapper needs to be inside a scene's view graph to have a window handle.
1 parent fed7ceb commit 2644ee1

File tree

6 files changed

+94
-4
lines changed

6 files changed

+94
-4
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/// Presents an alert to the user. Returns once an action has been selected and
2+
/// the corresponding action handler has been run. Returns the index of the
3+
/// selected action. By default, the alert will have a single button labelled
4+
/// `OK`. All buttons will dismiss the alert even if you provide your own
5+
/// actions.
6+
public struct PresentAlertAction {
7+
let environment: EnvironmentValues
8+
9+
@discardableResult
10+
public func callAsFunction(
11+
_ title: String,
12+
@AlertActionsBuilder actions: () -> [AlertAction] = { [.ok] }
13+
) async -> Int {
14+
let actions = actions()
15+
16+
func presentAlert<Backend: AppBackend>(backend: Backend) async -> Int {
17+
await withCheckedContinuation { continuation in
18+
backend.runInMainThread {
19+
let alert = backend.createAlert()
20+
backend.updateAlert(
21+
alert,
22+
title: title,
23+
actionLabels: actions.map(\.label),
24+
environment: environment
25+
)
26+
backend.showAlert(
27+
alert,
28+
window: environment.window! as! Backend.Window
29+
) { actionIndex in
30+
actions[actionIndex].action()
31+
continuation.resume(returning: actionIndex)
32+
}
33+
}
34+
}
35+
}
36+
37+
return await presentAlert(backend: environment.backend)
38+
}
39+
}

Sources/SwiftCrossUI/Environment/Actions/PresentFileSaveDialogAction.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Foundation
44
/// `nil` if the user cancels the operation.
55
public struct PresentFileSaveDialogAction {
66
let backend: any AppBackend
7-
let window: Any?
7+
let window: Any
88

99
public func callAsFunction(
1010
title: String = "Save",

Sources/SwiftCrossUI/Environment/Actions/PresentSingleFileOpenDialogAction.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import Foundation
55
/// in a single dialog. Returns `nil` if the user cancels the operation.
66
public struct PresentSingleFileOpenDialogAction {
77
let backend: any AppBackend
8-
let window: Any?
8+
let window: Any
99

1010
public func callAsFunction(
1111
title: String = "Open",

Sources/SwiftCrossUI/Environment/EnvironmentValues.swift

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,22 +47,66 @@ public struct EnvironmentValues {
4747
/// Presents an 'Open file' dialog fit for selecting a single file. Some
4848
/// backends only allow selecting either files or directories but not both
4949
/// in a single dialog. Returns `nil` if the user cancels the operation.
50+
/// Must not be accessed via the ``Environment`` property wrapper at the
51+
/// top-level of an ``App``. Only access it from ``View``s.
5052
public var chooseFile: PresentSingleFileOpenDialogAction {
53+
guard let window else {
54+
fatalError(
55+
"""
56+
chooseFile cannot be accessed at the top level of an app as \
57+
it must know which scene it's acting within. Access it from a \
58+
View implementation or use the `.alert` view modifier if you \
59+
want to present an alert directly within an App's body.
60+
"""
61+
)
62+
}
5163
return PresentSingleFileOpenDialogAction(
5264
backend: backend,
5365
window: window
5466
)
5567
}
5668

5769
/// Presents a 'Save file' dialog fit for selecting a save destination.
58-
/// Returns `nil` if the user cancels the operation.
70+
/// Returns `nil` if the user cancels the operation. Must not be accessed
71+
/// via the ``Environment`` property wrapper at the top-level of an ``App``.
72+
/// Only access it from ``View``s.
5973
public var chooseFileSaveDestination: PresentFileSaveDialogAction {
74+
guard let window else {
75+
fatalError(
76+
"""
77+
chooseFileSaveDestination cannot be accessed at the top level \
78+
of an app as it must know which scene it's acting within. \
79+
Access it from a View implementation or use the `.alert` view \
80+
modifier if you want to present an alert directly within an \
81+
App's body.
82+
"""
83+
)
84+
}
6085
return PresentFileSaveDialogAction(
6186
backend: backend,
6287
window: window
6388
)
6489
}
6590

91+
/// Presents an alert for the current window. Must not be accessed via
92+
/// the ``Environment`` property wrapper at the top-level of an ``App``.
93+
/// Only access it from ``View``s.
94+
public var presentAlert: PresentAlertAction {
95+
guard window != nil else {
96+
fatalError(
97+
"""
98+
presentAlert cannot be accessed at the top level of an app as \
99+
it must know which scene it's acting within. Access it from a \
100+
View implementation or use the `.alert` view modifier if you \
101+
want to present an alert directly within an App's body.
102+
"""
103+
)
104+
}
105+
return PresentAlertAction(
106+
environment: self
107+
)
108+
}
109+
66110
/// Creates the default environment.
67111
init<Backend: AppBackend>(backend: Backend) {
68112
self.backend = backend

Sources/SwiftCrossUI/Modifiers/AlertAction.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
/// breaking ``Button``'s API would have much more wide-reaching impacts than
66
/// breaking this single-purpose API.
77
public struct AlertAction {
8+
public static let ok = AlertAction(label: "Ok", action: {})
9+
810
public var label: String
911
public var action: () -> Void
1012
}

Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,13 @@ public final class WindowGroupNode<Content: View>: SceneGraphNode {
2424
environment: EnvironmentValues
2525
) {
2626
self.scene = scene
27-
viewGraph = ViewGraph(for: scene.body, backend: backend, environment: environment)
2827
let window = backend.createWindow(withDefaultSize: scene.defaultSize)
28+
29+
viewGraph = ViewGraph(
30+
for: scene.body,
31+
backend: backend,
32+
environment: environment.with(\.window, window)
33+
)
2934
let rootWidget = viewGraph.rootNode.concreteNode(for: Backend.self).widget
3035

3136
let container = backend.createContainer()

0 commit comments

Comments
 (0)