Skip to content

Commit 3108f77

Browse files
committed
UIKit sheet parent dismissals now dismiss both all children and the parent sheet itself
on AppKit its probably easier to let users handle this by just setting isPresented to false, as the implementation is quite different than on UIKit using windows instead of views. Maybe potential improvement later/in a different pr
1 parent b1e436b commit 3108f77

File tree

5 files changed

+143
-7
lines changed

5 files changed

+143
-7
lines changed

Examples/Sources/WindowingExample/WindowingApp.swift

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ struct AlertDemo: View {
6464
}
6565
}
6666

67+
// kind of a stress test for the dismiss action
6768
struct SheetDemo: View {
6869
@State var isPresented = false
6970
@State var isShortTermSheetPresented = false
@@ -85,7 +86,7 @@ struct SheetDemo: View {
8586
SheetBody()
8687
.presentationDetents([.height(150), .medium, .large])
8788
.presentationDragIndicatorVisibility(.visible)
88-
.presentationBackground(.blue)
89+
.presentationBackground(.green)
8990
}
9091
.sheet(isPresented: $isShortTermSheetPresented) {
9192
Text("I'm only here for 5s")
@@ -98,6 +99,7 @@ struct SheetDemo: View {
9899

99100
struct SheetBody: View {
100101
@State var isPresented = false
102+
@Environment(\.dismiss) var dismiss
101103

102104
var body: some View {
103105
VStack {
@@ -107,12 +109,51 @@ struct SheetDemo: View {
107109
isPresented = true
108110
print("should get presented")
109111
}
112+
Button("Dismiss") {
113+
dismiss()
114+
}
110115
Spacer()
111116
}
112117
.sheet(isPresented: $isPresented) {
113118
print("nested sheet dismissed")
114119
} content: {
120+
NestedSheetBody(dismissParent: { dismiss() })
121+
}
122+
}
123+
124+
struct NestedSheetBody: View {
125+
@Environment(\.dismiss) var dismiss
126+
var dismissParent: () -> Void
127+
@State var showNextChild = false
128+
129+
var body: some View {
115130
Text("I'm nested. Its claustrophobic in here.")
131+
Button("New Child Sheet") {
132+
showNextChild = true
133+
}
134+
.sheet(isPresented: $showNextChild) {
135+
DoubleNestedSheetBody(dismissParent: { dismiss() })
136+
}
137+
Button("dismiss parent sheet") {
138+
dismissParent()
139+
}
140+
Button("dismiss") {
141+
dismiss()
142+
}
143+
}
144+
}
145+
struct DoubleNestedSheetBody: View {
146+
@Environment(\.dismiss) var dismiss
147+
var dismissParent: () -> Void
148+
149+
var body: some View {
150+
Text("I'm nested. Its claustrophobic in here.")
151+
Button("dismiss parent sheet") {
152+
dismissParent()
153+
}
154+
Button("dismiss") {
155+
dismiss()
156+
}
116157
}
117158
}
118159
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/// An action that dismisses the current presentation context.
2+
///
3+
/// Use the `dismiss` environment value to get an instance of this action,
4+
/// then call it to dismiss the current sheet.
5+
///
6+
/// Example usage:
7+
/// ```swift
8+
/// struct SheetContentView: View {
9+
/// @Environment(\.dismiss) var dismiss
10+
///
11+
/// var body: some View {
12+
/// VStack {
13+
/// Text("Sheet Content")
14+
/// Button("Close") {
15+
/// dismiss()
16+
/// }
17+
/// }
18+
/// }
19+
/// }
20+
/// ```
21+
@MainActor
22+
public struct DismissAction {
23+
private let action: () -> Void
24+
25+
internal init(action: @escaping () -> Void) {
26+
self.action = action
27+
}
28+
29+
/// Dismisses the current presentation context.
30+
public func callAsFunction() {
31+
action()
32+
}
33+
}
34+
35+
/// Environment key for the dismiss action.
36+
private struct DismissActionKey: EnvironmentKey {
37+
@MainActor
38+
static var defaultValue: DismissAction {
39+
DismissAction(action: {
40+
#if DEBUG
41+
print("warning: dismiss() called but no presentation context is available")
42+
#endif
43+
})
44+
}
45+
}
46+
47+
extension EnvironmentValues {
48+
/// An action that dismisses the current presentation context.
49+
///
50+
/// Use this environment value to get a dismiss action that can be called
51+
/// to dismiss the current sheet, popover, or other presentation.
52+
///
53+
/// Example:
54+
/// ```swift
55+
/// struct ContentView: View {
56+
/// @Environment(\.dismiss) var dismiss
57+
///
58+
/// var body: some View {
59+
/// Button("Close") {
60+
/// dismiss()
61+
/// }
62+
/// }
63+
/// }
64+
/// ```
65+
@MainActor
66+
public var dismiss: DismissAction {
67+
get { self[DismissActionKey.self] }
68+
set { self[DismissActionKey.self] = newValue }
69+
}
70+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
public enum PresentationDragIndicatorVisibility {
1+
public enum PresentationDragIndicatorVisibility: Sendable {
22
case hidden, visible
33
}

Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,15 @@ struct SheetModifier<Content: View, SheetContent: View>: TypeSafeView {
7373
if isPresented.wrappedValue && children.sheet == nil {
7474
let sheet = backend.createSheet()
7575

76+
let dismissAction = DismissAction(action: { [isPresented] in
77+
isPresented.wrappedValue = false
78+
})
79+
let sheetEnvironment = environment.with(\.dismiss, dismissAction)
80+
7681
let dryRunResult = children.sheetContentNode.update(
7782
with: sheetContent(),
7883
proposedSize: sheet.size,
79-
environment: environment,
84+
environment: sheetEnvironment,
8085
dryRun: true
8186
)
8287

@@ -85,7 +90,7 @@ struct SheetModifier<Content: View, SheetContent: View>: TypeSafeView {
8590
let _ = children.sheetContentNode.update(
8691
with: sheetContent(),
8792
proposedSize: sheet.size,
88-
environment: environment,
93+
environment: sheetEnvironment,
8994
dryRun: false
9095
)
9196

@@ -95,7 +100,7 @@ struct SheetModifier<Content: View, SheetContent: View>: TypeSafeView {
95100
onDismiss: handleDismiss
96101
)
97102

98-
// Apply presentation preferences to the sheet
103+
// MARK: Sheet Presentation Preferences
99104
if let cornerRadius = preferences.presentationCornerRadius {
100105
backend.setPresentationCornerRadius(of: sheet, to: cornerRadius)
101106
}

Sources/UIKitBackend/UIKitBackend+Sheet.swift

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,15 @@ extension UIKitBackend {
2626
}
2727

2828
public func dismissSheet(_ sheet: CustomSheet, window: UIWindow?) {
29-
sheet.dismiss(animated: true)
29+
// If this sheet has a presented view controller (nested sheet), dismiss it first
30+
if let presentedVC = sheet.presentedViewController {
31+
presentedVC.dismiss(animated: false) { [weak sheet] in
32+
// After the nested sheet is dismissed, dismiss this sheet
33+
sheet?.dismissProgrammatically()
34+
}
35+
} else {
36+
sheet.dismissProgrammatically()
37+
}
3038
}
3139

3240
public func setPresentationDetents(of sheet: CustomSheet, to detents: [PresentationDetent]) {
@@ -110,12 +118,24 @@ public final class CustomSheet: UIViewController, SheetImplementation {
110118
}
111119

112120
var onDismiss: (() -> Void)?
121+
private var isDismissedProgrammatically = false
113122

114123
public override func viewDidLoad() {
115124
super.viewDidLoad()
116125
}
117126

127+
func dismissProgrammatically() {
128+
isDismissedProgrammatically = true
129+
dismiss(animated: true)
130+
}
131+
118132
public override func viewDidDisappear(_ animated: Bool) {
119-
onDismiss?()
133+
super.viewDidDisappear(animated)
134+
135+
// Only call onDismiss if the sheet was dismissed by user interaction (swipe down, tap outside)
136+
// not when dismissed programmatically via the dismiss action
137+
if !isDismissedProgrammatically {
138+
onDismiss?()
139+
}
120140
}
121141
}

0 commit comments

Comments
 (0)