Skip to content

Commit 6b5ca4a

Browse files
authored
Introduce sheet modifier (#219)
* Sheet presentation and nesting added to AppKitBackend * Added basic UIKit Sheet functionality and preparation for presentation modification * added presentationDetents and Radius to UIKitBackend * improved sheet rendering * added presentationDragIndicatorVisibility modifier * added presentationBackground * 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 * added interactiveDismissDisabled modifier * renamed size Parameter of SheetImplementation Protocol to sheetSize * GtkBackend Sheets save * finished GtkBackendSheets * documentation improvements * Should fix UIKitBackend compile issue with visionOS and AppBackend Conformance of WinUIBackend and Gtk3Backend * maybe ease gh actions gtk compile fix? * fixed winUI AppBackend Conformance * removed SheetImplementation Protocol, replaced with backend.sizeOf(_:) changed comment in appbackend * Adding sheet content in createSheet() instead of update (UIKit, AppKit) * adding sheet content on create (Gtk) * comment improvements * maybe part of letting scui dictate size? * changes dismissSheet now expects non optional Window rolled back proposed size try * moved signals to Gtk/Widgets/Window * removed now unecessary old code * mostly comment changes * onAppear now get called correctly, onDisappear only on interactive dismiss No idea why it doesn't get called on programmatic dismissal. Neither like it is now nor through a completion handler of the programmatic dismiss * should fix AppKit and Gtk interactive dismissal * using preferences from final result instead of dryRunResult * Fixed sheet rendering with GtkBackend on Linux * UIKit Sheet changes * Window uses EventControllerKey generated class instead of c interop * Replaced Window Escape key press c interop with generated class * Formatting and comments * Trying GdkModifierType instead of UInt * Made setPresentationBackground save to run/update n times on AppKitBackend * Fixed parent sheet dismissal leaves child sheet behind (GtkBackend testing pending) * beginCriticalSheet replaced with beginSheet on AppKitBackend * fix tvOS (and visionOS) compilation error * removed .formSheet modalPresentationStyle for tvOS 26 due to CI failure
1 parent 734b5df commit 6b5ca4a

File tree

17 files changed

+1176
-17
lines changed

17 files changed

+1176
-17
lines changed

Examples/Sources/WindowingExample/WindowingApp.swift

Lines changed: 127 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,112 @@ struct AlertDemo: View {
6464
}
6565
}
6666

67+
// A demo displaying SwiftCrossUI's `View.sheet` modifier.
68+
struct SheetDemo: View {
69+
@State var isPresented = false
70+
@State var isShortTermSheetPresented = false
71+
72+
var body: some View {
73+
Button("Open Sheet") {
74+
isPresented = true
75+
}
76+
Button("Show Sheet for 5s") {
77+
isShortTermSheetPresented = true
78+
Task {
79+
try? await Task.sleep(nanoseconds: 1_000_000_000 * 5)
80+
isShortTermSheetPresented = false
81+
}
82+
}
83+
.sheet(isPresented: $isPresented) {
84+
print("sheet dismissed")
85+
} content: {
86+
SheetBody()
87+
.presentationDetents([.height(150), .medium, .large])
88+
.presentationDragIndicatorVisibility(.visible)
89+
.presentationBackground(.green)
90+
}
91+
.sheet(isPresented: $isShortTermSheetPresented) {
92+
Text("I'm only here for 5s")
93+
.padding(20)
94+
.presentationDetents([.height(150), .medium, .large])
95+
.presentationCornerRadius(10)
96+
.presentationBackground(.red)
97+
}
98+
}
99+
100+
struct SheetBody: View {
101+
@State var isPresented = false
102+
@Environment(\.dismiss) var dismiss
103+
104+
var body: some View {
105+
VStack {
106+
Text("Nice sheet content")
107+
.padding(20)
108+
Button("I want more sheet") {
109+
isPresented = true
110+
print("should get presented")
111+
}
112+
Button("Dismiss") {
113+
dismiss()
114+
}
115+
Spacer()
116+
}
117+
.sheet(isPresented: $isPresented) {
118+
print("nested sheet dismissed")
119+
} content: {
120+
NestedSheetBody(dismissParent: { dismiss() })
121+
.presentationCornerRadius(35)
122+
}
123+
}
124+
125+
struct NestedSheetBody: View {
126+
@Environment(\.dismiss) var dismiss
127+
var dismissParent: () -> Void
128+
@State var showNextChild = false
129+
130+
var body: some View {
131+
Text("I'm nested. Its claustrophobic in here.")
132+
Button("New Child Sheet") {
133+
showNextChild = true
134+
}
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+
}
144+
}
145+
Button("dismiss parent sheet") {
146+
dismissParent()
147+
}
148+
Button("dismiss") {
149+
dismiss()
150+
}
151+
.onDisappear {
152+
print("nested sheet disappeared")
153+
}
154+
}
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+
}
168+
}
169+
}
170+
}
171+
}
172+
67173
@main
68174
@HotReloadable
69175
struct WindowingApp: App {
@@ -92,6 +198,11 @@ struct WindowingApp: App {
92198
Divider()
93199

94200
AlertDemo()
201+
202+
Divider()
203+
204+
SheetDemo()
205+
.padding(.bottom, 20)
95206
}
96207
.padding(20)
97208
}
@@ -108,23 +219,24 @@ struct WindowingApp: App {
108219
}
109220
}
110221
}
111-
112-
WindowGroup("Secondary window") {
113-
#hotReloadable {
114-
Text("This a secondary window!")
115-
.padding(10)
222+
#if !os(iOS) && !os(tvOS)
223+
WindowGroup("Secondary window") {
224+
#hotReloadable {
225+
Text("This a secondary window!")
226+
.padding(10)
227+
}
116228
}
117-
}
118-
.defaultSize(width: 200, height: 200)
119-
.windowResizability(.contentMinSize)
229+
.defaultSize(width: 200, height: 200)
230+
.windowResizability(.contentMinSize)
120231

121-
WindowGroup("Tertiary window") {
122-
#hotReloadable {
123-
Text("This a tertiary window!")
124-
.padding(10)
232+
WindowGroup("Tertiary window") {
233+
#hotReloadable {
234+
Text("This a tertiary window!")
235+
.padding(10)
236+
}
125237
}
126-
}
127-
.defaultSize(width: 200, height: 200)
128-
.windowResizability(.contentMinSize)
238+
.defaultSize(width: 200, height: 200)
239+
.windowResizability(.contentMinSize)
240+
#endif
129241
}
130242
}

Sources/AppKitBackend/AppKitBackend.swift

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public final class AppKitBackend: AppBackend {
1616
public typealias Menu = NSMenu
1717
public typealias Alert = NSAlert
1818
public typealias Path = NSBezierPath
19+
public typealias Sheet = NSCustomSheet
1920

2021
public let defaultTableRowContentHeight = 20
2122
public let defaultTableCellVerticalPadding = 4
@@ -1689,6 +1690,122 @@ public final class AppKitBackend: AppBackend {
16891690
let request = URLRequest(url: url)
16901691
webView.load(request)
16911692
}
1693+
1694+
public func createSheet(content: NSView) -> NSCustomSheet {
1695+
// Initialize with a default contentRect, similar to `createWindow`
1696+
let sheet = NSCustomSheet(
1697+
contentRect: NSRect(
1698+
x: 0,
1699+
y: 0,
1700+
width: 400, // Default width
1701+
height: 400 // Default height
1702+
),
1703+
styleMask: [.titled, .closable],
1704+
backing: .buffered,
1705+
defer: true
1706+
)
1707+
sheet.contentView = content
1708+
1709+
return sheet
1710+
}
1711+
1712+
public func updateSheet(
1713+
_ sheet: NSCustomSheet,
1714+
size: SIMD2<Int>,
1715+
onDismiss: @escaping () -> Void
1716+
) {
1717+
sheet.contentView?.frame.size = .init(width: size.x, height: size.y)
1718+
sheet.onDismiss = onDismiss
1719+
}
1720+
1721+
public func size(ofSheet sheet: NSCustomSheet) -> SIMD2<Int> {
1722+
guard let size = sheet.contentView?.frame.size else {
1723+
return SIMD2(x: 0, y: 0)
1724+
}
1725+
return SIMD2(x: Int(size.width), y: Int(size.height))
1726+
}
1727+
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)
1746+
}
1747+
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
1759+
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
1784+
}
1785+
}
1786+
1787+
public func setInteractiveDismissDisabled(for sheet: NSCustomSheet, to disabled: Bool) {
1788+
sheet.interactiveDismissDisabled = disabled
1789+
}
1790+
}
1791+
1792+
public final class NSCustomSheet: NSCustomWindow, NSWindowDelegate {
1793+
public var onDismiss: (() -> Void)?
1794+
1795+
public var interactiveDismissDisabled: Bool = false
1796+
1797+
public var backgroundView: NSView?
1798+
1799+
public func dismiss() {
1800+
onDismiss?()
1801+
self.contentViewController?.dismiss(self)
1802+
}
1803+
1804+
@objc override public func cancelOperation(_ sender: Any?) {
1805+
if !interactiveDismissDisabled {
1806+
dismiss()
1807+
}
1808+
}
16921809
}
16931810

16941811
final class NSCustomTapGestureTarget: NSView {
@@ -2111,6 +2228,8 @@ public class NSCustomWindow: NSWindow {
21112228
var customDelegate = Delegate()
21122229
var persistentUndoManager = UndoManager()
21132230

2231+
var managedAttachedSheet: NSCustomSheet?
2232+
21142233
/// Allows the backing scale factor to be overridden. Useful for keeping
21152234
/// UI tests consistent across devices.
21162235
///
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import CGtk
2+
3+
/// Provides access to key events.
4+
open class EventControllerKey: EventController {
5+
/// Creates a new event controller that will handle key events.
6+
public convenience init() {
7+
self.init(
8+
gtk_event_controller_key_new()
9+
)
10+
}
11+
12+
public override func registerSignals() {
13+
super.registerSignals()
14+
15+
addSignal(name: "im-update") { [weak self] () in
16+
guard let self = self else { return }
17+
self.imUpdate?(self)
18+
}
19+
20+
let handler1:
21+
@convention(c) (
22+
UnsafeMutableRawPointer, UInt, UInt, GdkModifierType, UnsafeMutableRawPointer
23+
) -> Void =
24+
{ _, value1, value2, value3, data in
25+
SignalBox3<UInt, UInt, GdkModifierType>.run(data, value1, value2, value3)
26+
}
27+
28+
addSignal(name: "key-pressed", handler: gCallback(handler1)) {
29+
[weak self] (param0: UInt, param1: UInt, param2: GdkModifierType) in
30+
guard let self = self else { return }
31+
self.keyPressed?(self, param0, param1, param2)
32+
}
33+
34+
let handler2:
35+
@convention(c) (
36+
UnsafeMutableRawPointer, UInt, UInt, GdkModifierType, UnsafeMutableRawPointer
37+
) -> Void =
38+
{ _, value1, value2, value3, data in
39+
SignalBox3<UInt, UInt, GdkModifierType>.run(data, value1, value2, value3)
40+
}
41+
42+
addSignal(name: "key-released", handler: gCallback(handler2)) {
43+
[weak self] (param0: UInt, param1: UInt, param2: GdkModifierType) in
44+
guard let self = self else { return }
45+
self.keyReleased?(self, param0, param1, param2)
46+
}
47+
48+
let handler3:
49+
@convention(c) (UnsafeMutableRawPointer, GdkModifierType, UnsafeMutableRawPointer) ->
50+
Void =
51+
{ _, value1, data in
52+
SignalBox1<GdkModifierType>.run(data, value1)
53+
}
54+
55+
addSignal(name: "modifiers", handler: gCallback(handler3)) {
56+
[weak self] (param0: GdkModifierType) in
57+
guard let self = self else { return }
58+
self.modifiers?(self, param0)
59+
}
60+
}
61+
62+
/// Emitted whenever the input method context filters away
63+
/// a keypress and prevents the @controller receiving it.
64+
///
65+
/// See [[email protected]_im_context] and
66+
/// [[email protected]_keypress].
67+
public var imUpdate: ((EventControllerKey) -> Void)?
68+
69+
/// Emitted whenever a key is pressed.
70+
public var keyPressed: ((EventControllerKey, UInt, UInt, GdkModifierType) -> Void)?
71+
72+
/// Emitted whenever a key is released.
73+
public var keyReleased: ((EventControllerKey, UInt, UInt, GdkModifierType) -> Void)?
74+
75+
/// Emitted whenever the state of modifier keys and pointer buttons change.
76+
public var modifiers: ((EventControllerKey, GdkModifierType) -> Void)?
77+
}

0 commit comments

Comments
 (0)