Skip to content

Commit a4ec00b

Browse files
committed
Implement sheet modifier PR feedback and ensure platform consistency
1 parent 54a545e commit a4ec00b

File tree

13 files changed

+491
-443
lines changed

13 files changed

+491
-443
lines changed

Examples/Sources/WindowingExample/WindowingApp.swift

Lines changed: 43 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -67,104 +67,90 @@ struct AlertDemo: View {
6767
// A demo displaying SwiftCrossUI's `View.sheet` modifier.
6868
struct SheetDemo: View {
6969
@State var isPresented = false
70-
@State var isShortTermSheetPresented = false
70+
@State var isEphemeralSheetPresented = false
71+
@State var ephemeralSheetDismissalTask: Task<Void, Never>?
7172

7273
var body: some View {
7374
Button("Open Sheet") {
7475
isPresented = true
7576
}
7677
Button("Show Sheet for 5s") {
77-
isShortTermSheetPresented = true
78-
Task {
79-
try? await Task.sleep(nanoseconds: 1_000_000_000 * 5)
80-
isShortTermSheetPresented = false
78+
isEphemeralSheetPresented = true
79+
ephemeralSheetDismissalTask = Task {
80+
do {
81+
try await Task.sleep(nanoseconds: 5 * 1_000_000_000)
82+
isEphemeralSheetPresented = false
83+
} catch {}
8184
}
8285
}
8386
.sheet(isPresented: $isPresented) {
84-
print("sheet dismissed")
87+
print("Root sheet dismissed")
8588
} content: {
8689
SheetBody()
87-
.presentationDetents([.height(150), .medium, .large])
88-
.presentationDragIndicatorVisibility(.visible)
90+
.presentationDetents([.height(250), .medium, .large])
8991
.presentationBackground(.green)
9092
}
91-
.sheet(isPresented: $isShortTermSheetPresented) {
93+
.sheet(isPresented: $isEphemeralSheetPresented) {
94+
ephemeralSheetDismissalTask?.cancel()
95+
} content: {
9296
Text("I'm only here for 5s")
9397
.padding(20)
94-
.presentationDetents([.height(150), .medium, .large])
98+
.presentationDetents([.medium])
9599
.presentationCornerRadius(10)
96100
.presentationBackground(.red)
97101
}
98102
}
99103

100104
struct SheetBody: View {
101-
@State var isPresented = false
105+
@State var isNestedSheetPresented = false
102106
@Environment(\.dismiss) var dismiss
103107

104108
var body: some View {
105109
VStack {
106-
Text("Nice sheet content")
107-
.padding(20)
108-
Button("I want more sheet") {
109-
isPresented = true
110-
print("should get presented")
110+
Text("Root sheet")
111+
Button("Present a nested sheet") {
112+
isNestedSheetPresented = true
111113
}
112114
Button("Dismiss") {
113115
dismiss()
114116
}
115-
Spacer()
116117
}
117-
.sheet(isPresented: $isPresented) {
118-
print("nested sheet dismissed")
118+
.padding()
119+
.sheet(isPresented: $isNestedSheetPresented) {
120+
print("Nested sheet dismissed")
119121
} content: {
120-
NestedSheetBody(dismissParent: { dismiss() })
121-
.presentationCornerRadius(35)
122+
NestedSheetBody(dismissRoot: { dismiss() })
123+
.presentationDetents([.height(250), .medium, .large])
122124
}
123125
}
126+
}
127+
128+
struct NestedSheetBody: View {
129+
var dismissRoot: () -> Void
124130

125-
struct NestedSheetBody: View {
126-
@Environment(\.dismiss) var dismiss
127-
var dismissParent: () -> Void
128-
@State var showNextChild = false
131+
@Environment(\.dismiss) var dismiss
132+
@State var showNextChild = false
133+
134+
var body: some View {
135+
VStack {
136+
Text("Nested sheet")
129137

130-
var body: some View {
131-
Text("I'm nested. Its claustrophobic in here.")
132-
Button("New Child Sheet") {
138+
Button("Present another sheet") {
133139
showNextChild = true
134140
}
135-
.sheet(isPresented: $showNextChild) {
136-
DoubleNestedSheetBody(dismissParent: { dismiss() })
137-
.interactiveDismissDisabled()
138-
.onAppear {
139-
print("deepest nested sheet appeared")
140-
}
141-
.onDisappear {
142-
print("deepest nested sheet disappeared")
143-
}
141+
Button("Dismiss root sheet") {
142+
dismissRoot()
144143
}
145-
Button("dismiss parent sheet") {
146-
dismissParent()
147-
}
148-
Button("dismiss") {
144+
Button("Dismiss") {
149145
dismiss()
150146
}
151-
.onDisappear {
152-
print("nested sheet disappeared")
153-
}
154147
}
155-
}
156-
struct DoubleNestedSheetBody: View {
157-
@Environment(\.dismiss) var dismiss
158-
var dismissParent: () -> Void
159-
160-
var body: some View {
161-
Text("I'm nested. Its claustrophobic in here.")
162-
Button("dismiss parent sheet") {
163-
dismissParent()
164-
}
165-
Button("dismiss") {
166-
dismiss()
167-
}
148+
.padding()
149+
.sheet(isPresented: $showNextChild) {
150+
print("Nested sheet dismissed")
151+
} content: {
152+
NestedSheetBody(dismissRoot: dismissRoot)
153+
.presentationDetents([.height(250), .medium, .large])
168154
}
169155
}
170156
}

Sources/AppKitBackend/AppKitBackend.swift

Lines changed: 66 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1697,25 +1697,59 @@ public final class AppKitBackend: AppBackend {
16971697
contentRect: NSRect(
16981698
x: 0,
16991699
y: 0,
1700-
width: 400, // Default width
1701-
height: 400 // Default height
1700+
width: 400,
1701+
height: 400
17021702
),
17031703
styleMask: [.titled, .closable],
17041704
backing: .buffered,
17051705
defer: true
17061706
)
1707-
sheet.contentView = content
1707+
1708+
let backgroundView = NSView()
1709+
backgroundView.translatesAutoresizingMaskIntoConstraints = false
1710+
backgroundView.wantsLayer = true
1711+
1712+
let contentView = NSView()
1713+
contentView.addSubview(backgroundView)
1714+
contentView.addSubview(content)
1715+
NSLayoutConstraint.activate([
1716+
contentView.topAnchor.constraint(equalTo: content.topAnchor),
1717+
contentView.leadingAnchor.constraint(equalTo: content.leadingAnchor),
1718+
contentView.bottomAnchor.constraint(equalTo: content.bottomAnchor),
1719+
contentView.trailingAnchor.constraint(equalTo: content.trailingAnchor),
1720+
contentView.topAnchor.constraint(equalTo: backgroundView.topAnchor),
1721+
contentView.leadingAnchor.constraint(equalTo: backgroundView.leadingAnchor),
1722+
contentView.bottomAnchor.constraint(equalTo: backgroundView.bottomAnchor),
1723+
contentView.trailingAnchor.constraint(equalTo: backgroundView.trailingAnchor),
1724+
])
1725+
contentView.translatesAutoresizingMaskIntoConstraints = false
1726+
1727+
sheet.contentView = contentView
1728+
sheet.backgroundView = backgroundView
17081729

17091730
return sheet
17101731
}
17111732

17121733
public func updateSheet(
17131734
_ sheet: NSCustomSheet,
17141735
size: SIMD2<Int>,
1715-
onDismiss: @escaping () -> Void
1736+
onDismiss: @escaping () -> Void,
1737+
cornerRadius: Double?,
1738+
detents: [PresentationDetent],
1739+
dragIndicatorVisibility: Visibility,
1740+
backgroundColor: Color?,
1741+
interactiveDismissDisabled: Bool
17161742
) {
1717-
sheet.contentView?.frame.size = .init(width: size.x, height: size.y)
1743+
sheet.setContentSize(NSSize(width: size.x, height: size.y))
17181744
sheet.onDismiss = onDismiss
1745+
1746+
let background = sheet.backgroundView!
1747+
background.layer?.backgroundColor = backgroundColor?.nsColor.cgColor
1748+
sheet.interactiveDismissDisabled = interactiveDismissDisabled
1749+
1750+
// - dragIndicatorVisibility is only for mobile so we ignore it
1751+
// - detents are only for mobile so we ignore them
1752+
// - cornerRadius isn't supported by macOS so we ignore it
17191753
}
17201754

17211755
public func size(ofSheet sheet: NSCustomSheet) -> SIMD2<Int> {
@@ -1725,67 +1759,32 @@ public final class AppKitBackend: AppBackend {
17251759
return SIMD2(x: Int(size.width), y: Int(size.height))
17261760
}
17271761

1728-
public func showSheet(_ sheet: NSCustomSheet, sheetParent: Any) {
1729-
// Critical sheets stack. beginSheet only shows a nested sheet
1730-
// after its parent gets dismissed.
1731-
let window = sheetParent as! NSCustomWindow
1732-
window.beginSheet(sheet)
1733-
window.managedAttachedSheet = sheet
1734-
}
1735-
1736-
public func dismissSheet(_ sheet: NSCustomSheet, sheetParent: Any) {
1737-
let window = sheetParent as! NSCustomWindow
1738-
1739-
if let nestedSheet = sheet.managedAttachedSheet {
1740-
dismissSheet(nestedSheet, sheetParent: sheet)
1741-
}
1742-
1743-
defer { window.managedAttachedSheet = nil }
1744-
1745-
window.endSheet(sheet)
1762+
public func presentSheet(_ sheet: NSCustomSheet, window: Window, parentSheet: Sheet?) {
1763+
let parent = parentSheet ?? window
1764+
// beginSheet and beginCriticalSheet should be equivalent here, because we
1765+
// directly present the sheet on top of the top-most sheet. If we were to
1766+
// instead present sheets on top of the root window every time, then
1767+
// beginCriticalSheet would produce the desired behaviour and beginSheet
1768+
// would wait for the parent sheet to finish before presenting the nested sheet.
1769+
parent.beginSheet(sheet)
1770+
parent.nestedSheet = sheet
17461771
}
17471772

1748-
public func setPresentationBackground(of sheet: NSCustomSheet, to color: Color) {
1749-
if let backgroundView = sheet.backgroundView {
1750-
backgroundView.layer?.backgroundColor = color.nsColor.cgColor
1751-
return
1752-
}
1753-
1754-
let backgroundView = NSView()
1755-
backgroundView.wantsLayer = true
1756-
backgroundView.layer?.backgroundColor = color.nsColor.cgColor
1757-
1758-
sheet.backgroundView = backgroundView
1773+
public func dismissSheet(_ sheet: NSCustomSheet, window: Window, parentSheet: Sheet?) {
1774+
let parent = parentSheet ?? window
17591775

1760-
if let existingContentView = sheet.contentView {
1761-
let container = NSView()
1762-
container.translatesAutoresizingMaskIntoConstraints = false
1763-
1764-
container.addSubview(backgroundView)
1765-
backgroundView.translatesAutoresizingMaskIntoConstraints = false
1766-
backgroundView.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive =
1767-
true
1768-
backgroundView.topAnchor.constraint(equalTo: container.topAnchor).isActive = true
1769-
backgroundView.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive =
1770-
true
1771-
backgroundView.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = true
1772-
1773-
container.addSubview(existingContentView)
1774-
existingContentView.translatesAutoresizingMaskIntoConstraints = false
1775-
existingContentView.leadingAnchor.constraint(equalTo: container.leadingAnchor)
1776-
.isActive = true
1777-
existingContentView.topAnchor.constraint(equalTo: container.topAnchor).isActive = true
1778-
existingContentView.trailingAnchor.constraint(equalTo: container.trailingAnchor)
1779-
.isActive = true
1780-
existingContentView.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive =
1781-
true
1782-
1783-
sheet.contentView = container
1776+
// Dismiss nested sheets first
1777+
if let nestedSheet = sheet.nestedSheet {
1778+
dismissSheet(nestedSheet, window: window, parentSheet: sheet)
1779+
// Although the current sheet has been dismissed programmatically,
1780+
// the nested sheets kind of haven't (at least, they weren't
1781+
// directly dismissed by SwiftCrossUI, so we must called onDismiss
1782+
// to let SwiftUI react to the dismissals of nested sheets).
1783+
nestedSheet.onDismiss?()
17841784
}
1785-
}
17861785

1787-
public func setInteractiveDismissDisabled(for sheet: NSCustomSheet, to disabled: Bool) {
1788-
sheet.interactiveDismissDisabled = disabled
1786+
parent.endSheet(sheet)
1787+
parent.nestedSheet = nil
17891788
}
17901789
}
17911790

@@ -1796,14 +1795,10 @@ public final class NSCustomSheet: NSCustomWindow, NSWindowDelegate {
17961795

17971796
public var backgroundView: NSView?
17981797

1799-
public func dismiss() {
1800-
onDismiss?()
1801-
self.contentViewController?.dismiss(self)
1802-
}
1803-
18041798
@objc override public func cancelOperation(_ sender: Any?) {
18051799
if !interactiveDismissDisabled {
1806-
dismiss()
1800+
sheetParent?.endSheet(self)
1801+
onDismiss?()
18071802
}
18081803
}
18091804
}
@@ -2228,7 +2223,10 @@ public class NSCustomWindow: NSWindow {
22282223
var customDelegate = Delegate()
22292224
var persistentUndoManager = UndoManager()
22302225

2231-
var managedAttachedSheet: NSCustomSheet?
2226+
/// A reference to the sheet currently presented on top of this window, if any.
2227+
/// If the sheet itself has another sheet presented on top of it, then that doubly
2228+
/// nested sheet gets stored as the sheet's nestedSheet, and so on.
2229+
var nestedSheet: NSCustomSheet?
22322230

22332231
/// Allows the backing scale factor to be overridden. Useful for keeping
22342232
/// UI tests consistent across devices.

Sources/Gtk/Widgets/Window.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import CGtk
77
open class Window: Widget {
88
public var child: Widget?
99

10-
public var managedAttachedSheet: Window?
11-
1210
public convenience init() {
1311
self.init(gtk_window_new())
1412
}
@@ -17,6 +15,7 @@ open class Window: Widget {
1715
@GObjectProperty(named: "resizable") public var resizable: Bool
1816
@GObjectProperty(named: "modal") public var isModal: Bool
1917
@GObjectProperty(named: "decorated") public var isDecorated: Bool
18+
@GObjectProperty(named: "destroy-with-parent") public var destroyWithParent: Bool
2019

2120
public func setTransient(for other: Window) {
2221
gtk_window_set_transient_for(castedPointer(), other.castedPointer())
@@ -89,6 +88,11 @@ open class Window: Widget {
8988
guard let self = self else { return }
9089
self.onCloseRequest?(self)
9190
}
91+
92+
addSignal(name: "destroy") { [weak self] () in
93+
guard let self = self else { return }
94+
self.onDestroy?(self)
95+
}
9296
}
9397

9498
public func setEscapeKeyPressedHandler(to handler: (() -> Void)?) {
@@ -108,6 +112,7 @@ open class Window: Widget {
108112
private var escapeKeyEventController: EventControllerKey?
109113

110114
public var onCloseRequest: ((Window) -> Void)?
115+
public var onDestroy: ((Window) -> Void)?
111116
public var escapeKeyPressed: (() -> Void)?
112117
}
113118

0 commit comments

Comments
 (0)