From d13cfaa81e1f489b4e925656cb8f9fe13e3ae4a4 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Fri, 3 Oct 2025 00:22:33 +0200 Subject: [PATCH 01/27] Sheet presentation and nesting added to AppKitBackend --- .../WindowingExample/WindowingApp.swift | 54 +++++++ Sources/AppKitBackend/AppKitBackend.swift | 62 ++++++++ Sources/SwiftCrossUI/Backend/AppBackend.swift | 53 +++++++ .../Views/Modifiers/SheetModifier.swift | 140 ++++++++++++++++++ 4 files changed, 309 insertions(+) create mode 100644 Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift diff --git a/Examples/Sources/WindowingExample/WindowingApp.swift b/Examples/Sources/WindowingExample/WindowingApp.swift index c17417b053..171d94bcd0 100644 --- a/Examples/Sources/WindowingExample/WindowingApp.swift +++ b/Examples/Sources/WindowingExample/WindowingApp.swift @@ -64,6 +64,56 @@ struct AlertDemo: View { } } +struct SheetDemo: View { + @State var isPresented = false + @State var isShortTermSheetPresented = false + + var body: some View { + Button("Open Sheet") { + isPresented = true + } + Button("Show Sheet for 5s") { + isShortTermSheetPresented = true + Task { + try? await Task.sleep(nanoseconds: 1_000_000_000 * 5) + isShortTermSheetPresented = false + } + } + .sheet(isPresented: $isPresented) { + print("sheet dismissed") + } content: { + SheetBody() + } + .sheet(isPresented: $isShortTermSheetPresented) { + Text("I'm only here for 5s") + .padding(20) + } + } + + struct SheetBody: View { + @State var isPresented = false + + var body: some View { + ZStack { + Color.blue + VStack { + Text("Nice sheet content") + .padding(20) + Button("I want more sheet") { + isPresented = true + print("should get presented") + } + } + } + .sheet(isPresented: $isPresented) { + print("nested sheet dismissed") + } content: { + Text("I'm nested. Its claustrophobic in here.") + } + } + } +} + @main @HotReloadable struct WindowingApp: App { @@ -92,6 +142,10 @@ struct WindowingApp: App { Divider() AlertDemo() + + Divider() + + SheetDemo() } .padding(20) } diff --git a/Sources/AppKitBackend/AppKitBackend.swift b/Sources/AppKitBackend/AppKitBackend.swift index 4f23de39d1..f351050e65 100644 --- a/Sources/AppKitBackend/AppKitBackend.swift +++ b/Sources/AppKitBackend/AppKitBackend.swift @@ -16,6 +16,7 @@ public final class AppKitBackend: AppBackend { public typealias Menu = NSMenu public typealias Alert = NSAlert public typealias Path = NSBezierPath + public typealias Sheet = NSCustomSheet public let defaultTableRowContentHeight = 20 public let defaultTableCellVerticalPadding = 4 @@ -1685,6 +1686,67 @@ public final class AppKitBackend: AppBackend { let request = URLRequest(url: url) webView.load(request) } + + public func createSheet() -> NSCustomSheet { + // Initialize with a default contentRect, similar to window creation (lines 58-68) + let sheet = NSCustomSheet( + contentRect: NSRect( + x: 0, + y: 0, + width: 400, // Default width + height: 300 // Default height + ), + styleMask: [.titled, .closable], + backing: .buffered, + defer: true + ) + return sheet + } + + public func updateSheet( + _ sheet: NSCustomSheet, content: NSView, onDismiss: @escaping () -> Void + ) { + let contentSize = naturalSize(of: content) + + let width = max(contentSize.x, 80) + let height = max(contentSize.y, 80) + sheet.setContentSize(NSSize(width: width, height: height)) + + sheet.contentView = content + sheet.onDismiss = onDismiss + } + + public func showSheet(_ sheet: NSCustomSheet, window: NSCustomWindow?) { + guard let window else { + print("warning: Cannot show sheet without a parent window") + return + } + // critical sheets stack + // beginSheet only shows a nested + // sheet after its parent gets dismissed + window.beginCriticalSheet(sheet) + } + + public func dismissSheet(_ sheet: NSCustomSheet, window: NSCustomWindow?) { + if let window { + window.endSheet(sheet) + } else { + NSApplication.shared.stopModal() + } + } +} + +public final class NSCustomSheet: NSCustomWindow, NSWindowDelegate { + public var onDismiss: (() -> Void)? + + public func dismiss() { + onDismiss?() + self.contentViewController?.dismiss(self) + } + + @objc override public func cancelOperation(_ sender: Any?) { + dismiss() + } } final class NSCustomTapGestureTarget: NSView { diff --git a/Sources/SwiftCrossUI/Backend/AppBackend.swift b/Sources/SwiftCrossUI/Backend/AppBackend.swift index 31a412cf2f..c4f852d09b 100644 --- a/Sources/SwiftCrossUI/Backend/AppBackend.swift +++ b/Sources/SwiftCrossUI/Backend/AppBackend.swift @@ -47,6 +47,7 @@ public protocol AppBackend: Sendable { associatedtype Menu associatedtype Alert associatedtype Path + associatedtype Sheet /// Creates an instance of the backend. init() @@ -603,6 +604,35 @@ public protocol AppBackend: Sendable { /// ``showAlert(_:window:responseHandler:)``. func dismissAlert(_ alert: Alert, window: Window?) + /// Creates a sheet object (without showing it yet). Sheets contain View Content. + /// They optionally execute provied code on dismiss and + /// prevent users from interacting with the parent window until dimissed. + func createSheet() -> Sheet + + /// Updates the content and appearance of a sheet + func updateSheet( + _ sheet: Sheet, + content: Widget, + onDismiss: @escaping () -> Void + ) + + /// Shows a sheet as a modal on top of or within the given window. + /// Users should be unable to interact with the parent window until the + /// sheet gets dismissed. The sheet will be closed once onDismiss gets called + /// + /// Must only get called once for any given sheet. + /// + /// If `window` is `nil`, the backend can either make the sheet a whole + /// app modal, a standalone window, or a modal for a window of its choosing. + func showSheet( + _ sheet: Sheet, + window: Window? + ) + + /// Dismisses a sheet programmatically. + /// Gets used by the SCUI sheet implementation to close a sheet. + func dismissSheet(_ sheet: Sheet, window: Window?) + /// Presents an 'Open file' dialog to the user for selecting files or /// folders. /// @@ -1162,4 +1192,27 @@ extension AppBackend { ) { todo() } + + public func createSheet() -> Sheet { + todo() + } + + public func updateSheet( + _ sheet: Sheet, + content: Widget, + onDismiss: @escaping () -> Void + ) { + todo() + } + + public func showSheet( + _ sheet: Sheet, + window: Window? + ) { + todo() + } + + public func dismissSheet(_ sheet: Sheet, window: Window?) { + todo() + } } diff --git a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift new file mode 100644 index 0000000000..a68f74d097 --- /dev/null +++ b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift @@ -0,0 +1,140 @@ +extension View { + /// presents a conditional modal overlay + /// onDismiss optional handler gets executed before + /// dismissing the sheet + public func sheet( + isPresented: Binding, onDismiss: (() -> Void)? = nil, + @ViewBuilder content: @escaping () -> SheetContent + ) -> some View { + SheetModifier( + isPresented: isPresented, body: TupleView1(self), onDismiss: onDismiss, + sheetContent: content) + } +} + +struct SheetModifier: TypeSafeView { + typealias Children = SheetModifierViewChildren + + var isPresented: Binding + var body: TupleView1 + var onDismiss: (() -> Void)? + var sheetContent: () -> SheetContent + + var sheet: Any? + + func children( + backend: Backend, + snapshots: [ViewGraphSnapshotter.NodeSnapshot]?, + environment: EnvironmentValues + ) -> Children { + let bodyViewGraphNode = ViewGraphNode( + for: body.view0, + backend: backend, + environment: environment + ) + let bodyNode = AnyViewGraphNode(bodyViewGraphNode) + + let sheetViewGraphNode = ViewGraphNode( + for: sheetContent(), + backend: backend, + environment: environment + ) + let sheetContentNode = AnyViewGraphNode(sheetViewGraphNode) + + return SheetModifierViewChildren( + childNode: bodyNode, + sheetContentNode: sheetContentNode, + sheet: nil + ) + } + + func asWidget( + _ children: Children, + backend: Backend + ) -> Backend.Widget { + children.childNode.widget.into() + } + + func update( + _ widget: Backend.Widget, + children: Children, + proposedSize: SIMD2, + environment: EnvironmentValues, + backend: Backend, + dryRun: Bool + ) -> ViewUpdateResult { + let childResult = children.childNode.update( + with: body.view0, + proposedSize: proposedSize, + environment: environment, + dryRun: dryRun + ) + + if isPresented.wrappedValue && children.sheet == nil { + let dryRunResult = children.sheetContentNode.update( + with: sheetContent(), + proposedSize: proposedSize, + environment: environment, + dryRun: true + ) + + let sheetSize = dryRunResult.size.idealSize + + let _ = children.sheetContentNode.update( + with: sheetContent(), + proposedSize: sheetSize, + environment: environment, + dryRun: false + ) + + let sheet = backend.createSheet() + + backend.updateSheet( + sheet, + content: children.sheetContentNode.widget.into(), + onDismiss: handleDismiss + ) + backend.showSheet( + sheet, + window: .some(environment.window! as! Backend.Window) + ) + children.sheet = sheet + } else if !isPresented.wrappedValue && children.sheet != nil { + backend.dismissSheet( + children.sheet as! Backend.Sheet, + window: .some(environment.window! as! Backend.Window) + ) + children.sheet = nil + } + return childResult + } + + func handleDismiss() { + onDismiss?() + isPresented.wrappedValue = false + } +} + +class SheetModifierViewChildren: ViewGraphNodeChildren { + var widgets: [AnyWidget] { + [childNode.widget] + } + + var erasedNodes: [ErasedViewGraphNode] { + [ErasedViewGraphNode(wrapping: childNode), ErasedViewGraphNode(wrapping: sheetContentNode)] + } + + var childNode: AnyViewGraphNode + var sheetContentNode: AnyViewGraphNode + var sheet: Any? + + init( + childNode: AnyViewGraphNode, + sheetContentNode: AnyViewGraphNode, + sheet: Any? + ) { + self.childNode = childNode + self.sheetContentNode = sheetContentNode + self.sheet = sheet + } +} From 19d772d9ace1f04b477381db97aeca81e2b6920c Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Fri, 3 Oct 2025 01:54:17 +0200 Subject: [PATCH 02/27] Added basic UIKit Sheet functionality and preparation for presentation modification --- .../WindowingExample/WindowingApp.swift | 32 +++++++------- Sources/AppKitBackend/AppKitBackend.swift | 4 ++ Sources/SwiftCrossUI/Backend/AppBackend.swift | 29 ++++++++++++ .../Values/PresentationDetent.swift | 16 +++++++ .../ViewGraph/PreferenceValues.swift | 23 +++++++++- .../Views/Modifiers/SheetModifier.swift | 13 ++++++ .../PresentationCornerRadiusModifier.swift | 19 ++++++++ .../Style/PresentationDetentsModifier.swift | 13 ++++++ Sources/UIKitBackend/UIKitBackend+Sheet.swift | 44 +++++++++++++++++++ 9 files changed, 176 insertions(+), 17 deletions(-) create mode 100644 Sources/SwiftCrossUI/Values/PresentationDetent.swift create mode 100644 Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationCornerRadiusModifier.swift create mode 100644 Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationDetentsModifier.swift create mode 100644 Sources/UIKitBackend/UIKitBackend+Sheet.swift diff --git a/Examples/Sources/WindowingExample/WindowingApp.swift b/Examples/Sources/WindowingExample/WindowingApp.swift index 171d94bcd0..f79986f2fe 100644 --- a/Examples/Sources/WindowingExample/WindowingApp.swift +++ b/Examples/Sources/WindowingExample/WindowingApp.swift @@ -87,6 +87,7 @@ struct SheetDemo: View { .sheet(isPresented: $isShortTermSheetPresented) { Text("I'm only here for 5s") .padding(20) + .presentationCornerRadius(2) } } @@ -162,23 +163,24 @@ struct WindowingApp: App { } } } - - WindowGroup("Secondary window") { - #hotReloadable { - Text("This a secondary window!") - .padding(10) + #if !os(iOS) + WindowGroup("Secondary window") { + #hotReloadable { + Text("This a secondary window!") + .padding(10) + } } - } - .defaultSize(width: 200, height: 200) - .windowResizability(.contentMinSize) + .defaultSize(width: 200, height: 200) + .windowResizability(.contentMinSize) - WindowGroup("Tertiary window") { - #hotReloadable { - Text("This a tertiary window!") - .padding(10) + WindowGroup("Tertiary window") { + #hotReloadable { + Text("This a tertiary window!") + .padding(10) + } } - } - .defaultSize(width: 200, height: 200) - .windowResizability(.contentMinSize) + .defaultSize(width: 200, height: 200) + .windowResizability(.contentMinSize) + #endif } } diff --git a/Sources/AppKitBackend/AppKitBackend.swift b/Sources/AppKitBackend/AppKitBackend.swift index f351050e65..e7d3102df2 100644 --- a/Sources/AppKitBackend/AppKitBackend.swift +++ b/Sources/AppKitBackend/AppKitBackend.swift @@ -1734,6 +1734,10 @@ public final class AppKitBackend: AppBackend { NSApplication.shared.stopModal() } } + + public func setPresentationCornerRadius(of sheet: NSCustomSheet, to radius: Int) { + print("setting Sheet Corner Radius is unavailable on macOS and will be ignored") + } } public final class NSCustomSheet: NSCustomWindow, NSWindowDelegate { diff --git a/Sources/SwiftCrossUI/Backend/AppBackend.swift b/Sources/SwiftCrossUI/Backend/AppBackend.swift index c4f852d09b..7155efcd47 100644 --- a/Sources/SwiftCrossUI/Backend/AppBackend.swift +++ b/Sources/SwiftCrossUI/Backend/AppBackend.swift @@ -633,6 +633,27 @@ public protocol AppBackend: Sendable { /// Gets used by the SCUI sheet implementation to close a sheet. func dismissSheet(_ sheet: Sheet, window: Window?) + /// Sets the corner radius for a sheet presentation. + /// + /// This method is called when the sheet content has a `presentationCornerRadius` modifier + /// applied at its top level. The corner radius affects the sheet's presentation container, + /// not the content itself. + /// + /// - Parameters: + /// - sheet: The sheet to apply the corner radius to. + /// - radius: The corner radius in pixels. + func setPresentationCornerRadius(of sheet: Sheet, to radius: Int) + + /// Sets the available detents (heights) for a sheet presentation. + /// + /// This method is called when the sheet content has a `presentationDetents` modifier + /// applied at its top level. Detents allow users to resize the sheet to predefined heights. + /// + /// - Parameters: + /// - sheet: The sheet to apply the detents to. + /// - detents: An array of detents that the sheet can be resized to. + func setPresentationDetents(of sheet: Sheet, to detents: [PresentationDetent]) + /// Presents an 'Open file' dialog to the user for selecting files or /// folders. /// @@ -1215,4 +1236,12 @@ extension AppBackend { public func dismissSheet(_ sheet: Sheet, window: Window?) { todo() } + + public func setPresentationCornerRadius(of sheet: Sheet, to radius: Int) { + todo() + } + + public func setPresentationDetents(of sheet: Sheet, to detents: [PresentationDetent]) { + todo() + } } diff --git a/Sources/SwiftCrossUI/Values/PresentationDetent.swift b/Sources/SwiftCrossUI/Values/PresentationDetent.swift new file mode 100644 index 0000000000..59eb6228fb --- /dev/null +++ b/Sources/SwiftCrossUI/Values/PresentationDetent.swift @@ -0,0 +1,16 @@ +/// Represents the available detents (heights) for a sheet presentation. +public enum PresentationDetent: Sendable, Hashable { + /// A detent that represents a medium height sheet. + case medium + + /// A detent that represents a large (full-height) sheet. + case large + + /// A detent at a custom fractional height of the available space. + /// - Parameter fraction: A value between 0 and 1 representing the fraction of available height. + case fraction(Double) + + /// A detent at a specific fixed height in pixels. + /// - Parameter height: The height in pixels. + case height(Int) +} diff --git a/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift b/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift index d03e497a39..d49496787b 100644 --- a/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift +++ b/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift @@ -2,13 +2,27 @@ import Foundation public struct PreferenceValues: Sendable { public static let `default` = PreferenceValues( - onOpenURL: nil + onOpenURL: nil, + presentationDetents: nil, + presentationCornerRadius: nil ) public var onOpenURL: (@Sendable @MainActor (URL) -> Void)? - public init(onOpenURL: (@Sendable @MainActor (URL) -> Void)?) { + /// The available detents for a sheet presentation. Only applies to the top-level view in a sheet. + public var presentationDetents: [PresentationDetent]? + + /// The corner radius for a sheet presentation. Only applies to the top-level view in a sheet. + public var presentationCornerRadius: Int? + + public init( + onOpenURL: (@Sendable @MainActor (URL) -> Void)?, + presentationDetents: [PresentationDetent]? = nil, + presentationCornerRadius: Int? = nil + ) { self.onOpenURL = onOpenURL + self.presentationDetents = presentationDetents + self.presentationCornerRadius = presentationCornerRadius } public init(merging children: [PreferenceValues]) { @@ -21,5 +35,10 @@ public struct PreferenceValues: Sendable { } } } + + // For presentation modifiers, take the first (top-level) value only + // This ensures only the root view's presentation modifiers apply to the sheet + presentationDetents = children.first?.presentationDetents + presentationCornerRadius = children.first?.presentationCornerRadius } } diff --git a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift index a68f74d097..7fda8c113d 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift @@ -78,6 +78,9 @@ struct SheetModifier: TypeSafeView { dryRun: true ) + // Extract preferences from the sheet content + let preferences = dryRunResult.preferences + let sheetSize = dryRunResult.size.idealSize let _ = children.sheetContentNode.update( @@ -94,6 +97,16 @@ struct SheetModifier: TypeSafeView { content: children.sheetContentNode.widget.into(), onDismiss: handleDismiss ) + + // Apply presentation preferences to the sheet + if let cornerRadius = preferences.presentationCornerRadius { + backend.setPresentationCornerRadius(of: sheet, to: cornerRadius) + } + + if let detents = preferences.presentationDetents { + backend.setPresentationDetents(of: sheet, to: detents) + } + backend.showSheet( sheet, window: .some(environment.window! as! Backend.Window) diff --git a/Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationCornerRadiusModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationCornerRadiusModifier.swift new file mode 100644 index 0000000000..4bfe4937d2 --- /dev/null +++ b/Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationCornerRadiusModifier.swift @@ -0,0 +1,19 @@ +// +// PresentationCornerRadiusModifier.swift +// swift-cross-ui +// +// Created by Mia Koring on 03.10.25. +// + +extension View { + /// Sets the corner radius for a sheet presentation. + /// + /// This modifier only affects the sheet presentation itself when applied to the + /// top-level view within a sheet. It does not affect the content's corner radius. + /// + /// - Parameter radius: The corner radius in pixels. + /// - Returns: A view with the presentation corner radius preference set. + public func presentationCornerRadius(_ radius: Int) -> some View { + preference(key: \.presentationCornerRadius, value: radius) + } +} diff --git a/Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationDetentsModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationDetentsModifier.swift new file mode 100644 index 0000000000..584b22ca33 --- /dev/null +++ b/Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationDetentsModifier.swift @@ -0,0 +1,13 @@ +extension View { + /// Sets the available detents (heights) for a sheet presentation. + /// + /// This modifier only affects the sheet presentation itself when applied to the + /// top-level view within a sheet. It allows users to resize the sheet to different + /// predefined heights. + /// + /// - Parameter detents: A set of detents that the sheet can be resized to. + /// - Returns: A view with the presentation detents preference set. + public func presentationDetents(_ detents: Set) -> some View { + preference(key: \.presentationDetents, value: Array(detents)) + } +} diff --git a/Sources/UIKitBackend/UIKitBackend+Sheet.swift b/Sources/UIKitBackend/UIKitBackend+Sheet.swift new file mode 100644 index 0000000000..188b49cdbf --- /dev/null +++ b/Sources/UIKitBackend/UIKitBackend+Sheet.swift @@ -0,0 +1,44 @@ +import SwiftCrossUI +import UIKit + +extension UIKitBackend { + public typealias Sheet = CustomSheet + + public func createSheet() -> CustomSheet { + let sheet = CustomSheet() + sheet.modalPresentationStyle = .formSheet + //sheet.transitioningDelegate = CustomSheetTransitioningDelegate() + + return sheet + } + + public func updateSheet(_ sheet: CustomSheet, content: Widget, onDismiss: @escaping () -> Void) + { + sheet.view = content.view + sheet.onDismiss = onDismiss + } + + public func showSheet(_ sheet: CustomSheet, window: UIWindow?) { + var topController = window?.rootViewController + while let presented = topController?.presentedViewController { + topController = presented + } + topController?.present(sheet, animated: true) + } + + public func dismissSheet(_ sheet: CustomSheet, window: UIWindow?) { + sheet.dismiss(animated: true) + } +} + +public final class CustomSheet: UIViewController { + var onDismiss: (() -> Void)? + + public override func viewDidLoad() { + super.viewDidLoad() + } + + public override func viewDidDisappear(_ animated: Bool) { + onDismiss?() + } +} From daca3e401af95fe8ce018be67ab8cb71a8357dee Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Fri, 3 Oct 2025 17:10:43 +0200 Subject: [PATCH 03/27] added presentationDetents and Radius to UIKitBackend --- .../WindowingExample/WindowingApp.swift | 4 +- Sources/AppKitBackend/AppKitBackend.swift | 4 -- Sources/SwiftCrossUI/Backend/AppBackend.swift | 16 ++++-- .../Values/PresentationDetent.swift | 6 ++- .../ViewGraph/PreferenceValues.swift | 4 +- .../PresentationCornerRadiusModifier.swift | 5 +- .../Style/PresentationDetentsModifier.swift | 4 ++ Sources/UIKitBackend/UIKitBackend+Sheet.swift | 54 ++++++++++++++++++- 8 files changed, 81 insertions(+), 16 deletions(-) diff --git a/Examples/Sources/WindowingExample/WindowingApp.swift b/Examples/Sources/WindowingExample/WindowingApp.swift index f79986f2fe..eccb058d40 100644 --- a/Examples/Sources/WindowingExample/WindowingApp.swift +++ b/Examples/Sources/WindowingExample/WindowingApp.swift @@ -87,7 +87,8 @@ struct SheetDemo: View { .sheet(isPresented: $isShortTermSheetPresented) { Text("I'm only here for 5s") .padding(20) - .presentationCornerRadius(2) + .presentationDetents([.height(150), .medium, .large]) + .presentationCornerRadius(10) } } @@ -147,6 +148,7 @@ struct WindowingApp: App { Divider() SheetDemo() + .padding(.bottom, 20) } .padding(20) } diff --git a/Sources/AppKitBackend/AppKitBackend.swift b/Sources/AppKitBackend/AppKitBackend.swift index e7d3102df2..f351050e65 100644 --- a/Sources/AppKitBackend/AppKitBackend.swift +++ b/Sources/AppKitBackend/AppKitBackend.swift @@ -1734,10 +1734,6 @@ public final class AppKitBackend: AppBackend { NSApplication.shared.stopModal() } } - - public func setPresentationCornerRadius(of sheet: NSCustomSheet, to radius: Int) { - print("setting Sheet Corner Radius is unavailable on macOS and will be ignored") - } } public final class NSCustomSheet: NSCustomWindow, NSWindowDelegate { diff --git a/Sources/SwiftCrossUI/Backend/AppBackend.swift b/Sources/SwiftCrossUI/Backend/AppBackend.swift index 7155efcd47..5cf88dbbd8 100644 --- a/Sources/SwiftCrossUI/Backend/AppBackend.swift +++ b/Sources/SwiftCrossUI/Backend/AppBackend.swift @@ -641,8 +641,8 @@ public protocol AppBackend: Sendable { /// /// - Parameters: /// - sheet: The sheet to apply the corner radius to. - /// - radius: The corner radius in pixels. - func setPresentationCornerRadius(of sheet: Sheet, to radius: Int) + /// - radius: The corner radius + func setPresentationCornerRadius(of sheet: Sheet, to radius: Double) /// Sets the available detents (heights) for a sheet presentation. /// @@ -778,6 +778,12 @@ extension AppBackend { Foundation.exit(1) } + private func ignored(_ function: String = #function) { + print( + "\(type(of: self)): \(function) is being ignored\nConsult at the documentation for further information." + ) + } + // MARK: System public func openExternalURL(_ url: URL) throws { @@ -1237,11 +1243,11 @@ extension AppBackend { todo() } - public func setPresentationCornerRadius(of sheet: Sheet, to radius: Int) { - todo() + public func setPresentationCornerRadius(of sheet: Sheet, to radius: Double) { + ignored() } public func setPresentationDetents(of sheet: Sheet, to detents: [PresentationDetent]) { - todo() + ignored() } } diff --git a/Sources/SwiftCrossUI/Values/PresentationDetent.swift b/Sources/SwiftCrossUI/Values/PresentationDetent.swift index 59eb6228fb..a8b24c5782 100644 --- a/Sources/SwiftCrossUI/Values/PresentationDetent.swift +++ b/Sources/SwiftCrossUI/Values/PresentationDetent.swift @@ -7,10 +7,12 @@ public enum PresentationDetent: Sendable, Hashable { case large /// A detent at a custom fractional height of the available space. + /// falling back to medium on iOS 15 /// - Parameter fraction: A value between 0 and 1 representing the fraction of available height. case fraction(Double) /// A detent at a specific fixed height in pixels. - /// - Parameter height: The height in pixels. - case height(Int) + /// falling back to medium on iOS 15 + /// - Parameter height: The height + case height(Double) } diff --git a/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift b/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift index d49496787b..d3d9c52075 100644 --- a/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift +++ b/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift @@ -13,12 +13,12 @@ public struct PreferenceValues: Sendable { public var presentationDetents: [PresentationDetent]? /// The corner radius for a sheet presentation. Only applies to the top-level view in a sheet. - public var presentationCornerRadius: Int? + public var presentationCornerRadius: Double? public init( onOpenURL: (@Sendable @MainActor (URL) -> Void)?, presentationDetents: [PresentationDetent]? = nil, - presentationCornerRadius: Int? = nil + presentationCornerRadius: Double? = nil ) { self.onOpenURL = onOpenURL self.presentationDetents = presentationDetents diff --git a/Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationCornerRadiusModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationCornerRadiusModifier.swift index 4bfe4937d2..46a72075cc 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationCornerRadiusModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationCornerRadiusModifier.swift @@ -11,9 +11,12 @@ extension View { /// This modifier only affects the sheet presentation itself when applied to the /// top-level view within a sheet. It does not affect the content's corner radius. /// + /// supported platforms: iOS (ignored on unsupported platforms) + /// ignored on: older than iOS 15 + /// /// - Parameter radius: The corner radius in pixels. /// - Returns: A view with the presentation corner radius preference set. - public func presentationCornerRadius(_ radius: Int) -> some View { + public func presentationCornerRadius(_ radius: Double) -> some View { preference(key: \.presentationCornerRadius, value: radius) } } diff --git a/Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationDetentsModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationDetentsModifier.swift index 584b22ca33..78afc5cd2d 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationDetentsModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationDetentsModifier.swift @@ -5,6 +5,10 @@ extension View { /// top-level view within a sheet. It allows users to resize the sheet to different /// predefined heights. /// + /// supported platforms: iOS (ignored on unsupported platforms) + /// ignored on: older than iOS 15 + /// fraction and height fall back to medium on iOS 15 and work as you'd expect on >=16 + /// /// - Parameter detents: A set of detents that the sheet can be resized to. /// - Returns: A view with the presentation detents preference set. public func presentationDetents(_ detents: Set) -> some View { diff --git a/Sources/UIKitBackend/UIKitBackend+Sheet.swift b/Sources/UIKitBackend/UIKitBackend+Sheet.swift index 188b49cdbf..cb8853b05f 100644 --- a/Sources/UIKitBackend/UIKitBackend+Sheet.swift +++ b/Sources/UIKitBackend/UIKitBackend+Sheet.swift @@ -7,7 +7,6 @@ extension UIKitBackend { public func createSheet() -> CustomSheet { let sheet = CustomSheet() sheet.modalPresentationStyle = .formSheet - //sheet.transitioningDelegate = CustomSheetTransitioningDelegate() return sheet } @@ -29,6 +28,59 @@ extension UIKitBackend { public func dismissSheet(_ sheet: CustomSheet, window: UIWindow?) { sheet.dismiss(animated: true) } + + public func setPresentationDetents(of sheet: CustomSheet, to detents: [PresentationDetent]) { + if #available(iOS 15.0, *) { + if let sheetPresentation = sheet.sheetPresentationController { + sheetPresentation.detents = detents.map { + switch $0 { + case .medium: return .medium() + case .large: return .large() + case .fraction(let fraction): + if #available(iOS 16.0, *) { + return .custom( + identifier: .init("Fraction:\(fraction)"), + resolver: { context in + context.maximumDetentValue * fraction + }) + } else { + return .medium() + } + case .height(let height): + if #available(iOS 16.0, *) { + return .custom( + identifier: .init("Height:\(height)"), + resolver: { context in + height + }) + } else { + return .medium() + } + } + } + } + } else { + #if DEBUG + print( + "your current OS Version doesn't support variable sheet heights.\n Setting presentationDetents is only available from iOS 15.0" + ) + #endif + } + } + + public func setPresentationCornerRadius(of sheet: CustomSheet, to radius: Double) { + if #available(iOS 15.0, *) { + if let sheetController = sheet.sheetPresentationController { + sheetController.preferredCornerRadius = radius + } + } else { + #if DEBUG + print( + "your current OS Version doesn't support variable sheet corner radii.\n Setting them is only available from iOS 15.0" + ) + #endif + } + } } public final class CustomSheet: UIViewController { From d6a63f2d0d53e54633b4f4cd6e97bf4c746460d3 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Fri, 3 Oct 2025 19:05:52 +0200 Subject: [PATCH 04/27] improved sheet rendering --- .../Sources/WindowingExample/WindowingApp.swift | 3 +++ Sources/AppKitBackend/AppKitBackend.swift | 8 +++++++- Sources/SwiftCrossUI/Backend/AppBackend.swift | 6 +++++- .../Views/Modifiers/SheetModifier.swift | 13 ++++++------- Sources/UIKitBackend/UIKitBackend+Sheet.swift | 7 ++++++- 5 files changed, 27 insertions(+), 10 deletions(-) diff --git a/Examples/Sources/WindowingExample/WindowingApp.swift b/Examples/Sources/WindowingExample/WindowingApp.swift index eccb058d40..e478c0dbfa 100644 --- a/Examples/Sources/WindowingExample/WindowingApp.swift +++ b/Examples/Sources/WindowingExample/WindowingApp.swift @@ -83,6 +83,8 @@ struct SheetDemo: View { print("sheet dismissed") } content: { SheetBody() + .frame(maxWidth: 200, maxHeight: 100) + .presentationDetents([.height(150), .medium, .large]) } .sheet(isPresented: $isShortTermSheetPresented) { Text("I'm only here for 5s") @@ -105,6 +107,7 @@ struct SheetDemo: View { isPresented = true print("should get presented") } + Spacer() } } .sheet(isPresented: $isPresented) { diff --git a/Sources/AppKitBackend/AppKitBackend.swift b/Sources/AppKitBackend/AppKitBackend.swift index f351050e65..1d7fcb58af 100644 --- a/Sources/AppKitBackend/AppKitBackend.swift +++ b/Sources/AppKitBackend/AppKitBackend.swift @@ -1736,7 +1736,13 @@ public final class AppKitBackend: AppBackend { } } -public final class NSCustomSheet: NSCustomWindow, NSWindowDelegate { +public final class NSCustomSheet: NSCustomWindow, NSWindowDelegate, SheetImplementation { + public var size: SIMD2 { + guard let size = self.contentView?.frame.size else { + return SIMD2(x: 0, y: 0) + } + return SIMD2(x: Int(size.width), y: Int(size.height)) + } public var onDismiss: (() -> Void)? public func dismiss() { diff --git a/Sources/SwiftCrossUI/Backend/AppBackend.swift b/Sources/SwiftCrossUI/Backend/AppBackend.swift index 5cf88dbbd8..e5412c982d 100644 --- a/Sources/SwiftCrossUI/Backend/AppBackend.swift +++ b/Sources/SwiftCrossUI/Backend/AppBackend.swift @@ -47,7 +47,7 @@ public protocol AppBackend: Sendable { associatedtype Menu associatedtype Alert associatedtype Path - associatedtype Sheet + associatedtype Sheet: SheetImplementation /// Creates an instance of the backend. init() @@ -771,6 +771,10 @@ extension AppBackend { } } +public protocol SheetImplementation { + var size: SIMD2 { get } +} + extension AppBackend { /// Used by placeholder implementations of backend methods. private func todo(_ function: String = #function) -> Never { diff --git a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift index 7fda8c113d..2dad1d17ba 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift @@ -71,27 +71,26 @@ struct SheetModifier: TypeSafeView { ) if isPresented.wrappedValue && children.sheet == nil { + //let sheetSize = dryRunResult.size.idealSize + + let sheet = backend.createSheet() + let dryRunResult = children.sheetContentNode.update( with: sheetContent(), - proposedSize: proposedSize, + proposedSize: sheet.size, environment: environment, dryRun: true ) - // Extract preferences from the sheet content let preferences = dryRunResult.preferences - let sheetSize = dryRunResult.size.idealSize - let _ = children.sheetContentNode.update( with: sheetContent(), - proposedSize: sheetSize, + proposedSize: sheet.size, environment: environment, dryRun: false ) - let sheet = backend.createSheet() - backend.updateSheet( sheet, content: children.sheetContentNode.widget.into(), diff --git a/Sources/UIKitBackend/UIKitBackend+Sheet.swift b/Sources/UIKitBackend/UIKitBackend+Sheet.swift index cb8853b05f..3a3f8c4de4 100644 --- a/Sources/UIKitBackend/UIKitBackend+Sheet.swift +++ b/Sources/UIKitBackend/UIKitBackend+Sheet.swift @@ -83,7 +83,12 @@ extension UIKitBackend { } } -public final class CustomSheet: UIViewController { +public final class CustomSheet: UIViewController, SheetImplementation { + public var size: SIMD2 { + let size = view.frame.size + return SIMD2(x: Int(size.width), y: Int(size.height)) + } + var onDismiss: (() -> Void)? public override func viewDidLoad() { From 79837ed81dc0fc062089b56419baa5be9b5c10d2 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Fri, 3 Oct 2025 19:44:51 +0200 Subject: [PATCH 05/27] added presentationDragIndicatorVisibility modifier --- .../WindowingExample/WindowingApp.swift | 2 +- Sources/SwiftCrossUI/Backend/AppBackend.swift | 9 +++++ .../PresentationDragIndicatorVisibility.swift | 3 ++ .../ViewGraph/PreferenceValues.swift | 6 ++- .../Views/Modifiers/SheetModifier.swift | 9 ++++- .../PresentationCornerRadiusModifier.swift | 22 ----------- .../Style/PresentationDetentsModifier.swift | 17 --------- .../Style/PresentationModifiers.swift | 37 +++++++++++++++++++ Sources/UIKitBackend/UIKitBackend+Sheet.swift | 16 ++++++++ 9 files changed, 78 insertions(+), 43 deletions(-) create mode 100644 Sources/SwiftCrossUI/Values/PresentationDragIndicatorVisibility.swift delete mode 100644 Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationCornerRadiusModifier.swift delete mode 100644 Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationDetentsModifier.swift create mode 100644 Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationModifiers.swift diff --git a/Examples/Sources/WindowingExample/WindowingApp.swift b/Examples/Sources/WindowingExample/WindowingApp.swift index e478c0dbfa..3d230d83b5 100644 --- a/Examples/Sources/WindowingExample/WindowingApp.swift +++ b/Examples/Sources/WindowingExample/WindowingApp.swift @@ -83,8 +83,8 @@ struct SheetDemo: View { print("sheet dismissed") } content: { SheetBody() - .frame(maxWidth: 200, maxHeight: 100) .presentationDetents([.height(150), .medium, .large]) + .presentationDragIndicatorVisibility(.visible) } .sheet(isPresented: $isShortTermSheetPresented) { Text("I'm only here for 5s") diff --git a/Sources/SwiftCrossUI/Backend/AppBackend.swift b/Sources/SwiftCrossUI/Backend/AppBackend.swift index e5412c982d..6f587700c7 100644 --- a/Sources/SwiftCrossUI/Backend/AppBackend.swift +++ b/Sources/SwiftCrossUI/Backend/AppBackend.swift @@ -654,6 +654,9 @@ public protocol AppBackend: Sendable { /// - detents: An array of detents that the sheet can be resized to. func setPresentationDetents(of sheet: Sheet, to detents: [PresentationDetent]) + func setPresentationDragIndicatorVisibility( + of sheet: Sheet, to visibility: PresentationDragIndicatorVisibility) + /// Presents an 'Open file' dialog to the user for selecting files or /// folders. /// @@ -1254,4 +1257,10 @@ extension AppBackend { public func setPresentationDetents(of sheet: Sheet, to detents: [PresentationDetent]) { ignored() } + + public func setPresentationDragIndicatorVisibility( + of sheet: Sheet, to visibility: PresentationDragIndicatorVisibility + ) { + ignored() + } } diff --git a/Sources/SwiftCrossUI/Values/PresentationDragIndicatorVisibility.swift b/Sources/SwiftCrossUI/Values/PresentationDragIndicatorVisibility.swift new file mode 100644 index 0000000000..60151bff6a --- /dev/null +++ b/Sources/SwiftCrossUI/Values/PresentationDragIndicatorVisibility.swift @@ -0,0 +1,3 @@ +public enum PresentationDragIndicatorVisibility { + case hidden, visible +} diff --git a/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift b/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift index d3d9c52075..519b6b59d2 100644 --- a/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift +++ b/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift @@ -15,10 +15,13 @@ public struct PreferenceValues: Sendable { /// The corner radius for a sheet presentation. Only applies to the top-level view in a sheet. public var presentationCornerRadius: Double? + public var presentationDragIndicatorVisibility: PresentationDragIndicatorVisibility? + public init( onOpenURL: (@Sendable @MainActor (URL) -> Void)?, presentationDetents: [PresentationDetent]? = nil, - presentationCornerRadius: Double? = nil + presentationCornerRadius: Double? = nil, + presentationDragIndicatorVisibility: PresentationDragIndicatorVisibility? = nil ) { self.onOpenURL = onOpenURL self.presentationDetents = presentationDetents @@ -40,5 +43,6 @@ public struct PreferenceValues: Sendable { // This ensures only the root view's presentation modifiers apply to the sheet presentationDetents = children.first?.presentationDetents presentationCornerRadius = children.first?.presentationCornerRadius + presentationDragIndicatorVisibility = children.first?.presentationDragIndicatorVisibility } } diff --git a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift index 2dad1d17ba..0e36ffb125 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift @@ -71,8 +71,6 @@ struct SheetModifier: TypeSafeView { ) if isPresented.wrappedValue && children.sheet == nil { - //let sheetSize = dryRunResult.size.idealSize - let sheet = backend.createSheet() let dryRunResult = children.sheetContentNode.update( @@ -106,6 +104,13 @@ struct SheetModifier: TypeSafeView { backend.setPresentationDetents(of: sheet, to: detents) } + if let presentationDragIndicatorVisibility = preferences + .presentationDragIndicatorVisibility + { + backend.setPresentationDragIndicatorVisibility( + of: sheet, to: presentationDragIndicatorVisibility) + } + backend.showSheet( sheet, window: .some(environment.window! as! Backend.Window) diff --git a/Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationCornerRadiusModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationCornerRadiusModifier.swift deleted file mode 100644 index 46a72075cc..0000000000 --- a/Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationCornerRadiusModifier.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// PresentationCornerRadiusModifier.swift -// swift-cross-ui -// -// Created by Mia Koring on 03.10.25. -// - -extension View { - /// Sets the corner radius for a sheet presentation. - /// - /// This modifier only affects the sheet presentation itself when applied to the - /// top-level view within a sheet. It does not affect the content's corner radius. - /// - /// supported platforms: iOS (ignored on unsupported platforms) - /// ignored on: older than iOS 15 - /// - /// - Parameter radius: The corner radius in pixels. - /// - Returns: A view with the presentation corner radius preference set. - public func presentationCornerRadius(_ radius: Double) -> some View { - preference(key: \.presentationCornerRadius, value: radius) - } -} diff --git a/Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationDetentsModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationDetentsModifier.swift deleted file mode 100644 index 78afc5cd2d..0000000000 --- a/Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationDetentsModifier.swift +++ /dev/null @@ -1,17 +0,0 @@ -extension View { - /// Sets the available detents (heights) for a sheet presentation. - /// - /// This modifier only affects the sheet presentation itself when applied to the - /// top-level view within a sheet. It allows users to resize the sheet to different - /// predefined heights. - /// - /// supported platforms: iOS (ignored on unsupported platforms) - /// ignored on: older than iOS 15 - /// fraction and height fall back to medium on iOS 15 and work as you'd expect on >=16 - /// - /// - Parameter detents: A set of detents that the sheet can be resized to. - /// - Returns: A view with the presentation detents preference set. - public func presentationDetents(_ detents: Set) -> some View { - preference(key: \.presentationDetents, value: Array(detents)) - } -} diff --git a/Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationModifiers.swift b/Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationModifiers.swift new file mode 100644 index 0000000000..512095b0f7 --- /dev/null +++ b/Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationModifiers.swift @@ -0,0 +1,37 @@ +extension View { + /// Sets the available detents (heights) for a sheet presentation. + /// + /// This modifier only affects the sheet presentation itself when applied to the + /// top-level view within a sheet. It allows users to resize the sheet to different + /// predefined heights. + /// + /// supported platforms: iOS (ignored on unsupported platforms) + /// ignored on: older than iOS 15 + /// fraction and height fall back to medium on iOS 15 and work as you'd expect on >=16 + /// + /// - Parameter detents: A set of detents that the sheet can be resized to. + /// - Returns: A view with the presentation detents preference set. + public func presentationDetents(_ detents: Set) -> some View { + preference(key: \.presentationDetents, value: Array(detents)) + } + + /// Sets the corner radius for a sheet presentation. + /// + /// This modifier only affects the sheet presentation itself when applied to the + /// top-level view within a sheet. It does not affect the content's corner radius. + /// + /// supported platforms: iOS (ignored on unsupported platforms) + /// ignored on: older than iOS 15 + /// + /// - Parameter radius: The corner radius in pixels. + /// - Returns: A view with the presentation corner radius preference set. + public func presentationCornerRadius(_ radius: Double) -> some View { + preference(key: \.presentationCornerRadius, value: radius) + } + + public func presentationDragIndicatorVisibility( + _ visibility: PresentationDragIndicatorVisibility + ) -> some View { + preference(key: \.presentationDragIndicatorVisibility, value: visibility) + } +} diff --git a/Sources/UIKitBackend/UIKitBackend+Sheet.swift b/Sources/UIKitBackend/UIKitBackend+Sheet.swift index 3a3f8c4de4..a13de8e97b 100644 --- a/Sources/UIKitBackend/UIKitBackend+Sheet.swift +++ b/Sources/UIKitBackend/UIKitBackend+Sheet.swift @@ -81,6 +81,22 @@ extension UIKitBackend { #endif } } + + public func setPresentationDragIndicatorVisibility( + of sheet: Sheet, to visibility: PresentationDragIndicatorVisibility + ) { + if #available(iOS 15.0, *) { + if let sheetController = sheet.sheetPresentationController { + sheetController.prefersGrabberVisible = visibility == .visible ? true : false + } + } else { + #if DEBUG + print( + "Your current OS Version doesn't support setting sheet drag indicator visibility.\n Setting this is only available from iOS 15.0" + ) + #endif + } + } } public final class CustomSheet: UIViewController, SheetImplementation { From b1e436b33886c21db96374f27d572a13cdbd3cdf Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Fri, 3 Oct 2025 21:13:08 +0200 Subject: [PATCH 06/27] added presentationBackground --- .../WindowingExample/WindowingApp.swift | 19 +++++----- Sources/AppKitBackend/AppKitBackend.swift | 36 +++++++++++++++++-- Sources/SwiftCrossUI/Backend/AppBackend.swift | 34 +++++++++++++++--- .../ViewGraph/PreferenceValues.swift | 13 +++++-- .../Views/Modifiers/SheetModifier.swift | 4 +++ .../Style/PresentationModifiers.swift | 4 +++ Sources/UIKitBackend/UIKitBackend+Sheet.swift | 4 +++ 7 files changed, 96 insertions(+), 18 deletions(-) diff --git a/Examples/Sources/WindowingExample/WindowingApp.swift b/Examples/Sources/WindowingExample/WindowingApp.swift index 3d230d83b5..f878c3451d 100644 --- a/Examples/Sources/WindowingExample/WindowingApp.swift +++ b/Examples/Sources/WindowingExample/WindowingApp.swift @@ -85,12 +85,14 @@ struct SheetDemo: View { SheetBody() .presentationDetents([.height(150), .medium, .large]) .presentationDragIndicatorVisibility(.visible) + .presentationBackground(.blue) } .sheet(isPresented: $isShortTermSheetPresented) { Text("I'm only here for 5s") .padding(20) .presentationDetents([.height(150), .medium, .large]) .presentationCornerRadius(10) + .presentationBackground(.red) } } @@ -98,17 +100,14 @@ struct SheetDemo: View { @State var isPresented = false var body: some View { - ZStack { - Color.blue - VStack { - Text("Nice sheet content") - .padding(20) - Button("I want more sheet") { - isPresented = true - print("should get presented") - } - Spacer() + VStack { + Text("Nice sheet content") + .padding(20) + Button("I want more sheet") { + isPresented = true + print("should get presented") } + Spacer() } .sheet(isPresented: $isPresented) { print("nested sheet dismissed") diff --git a/Sources/AppKitBackend/AppKitBackend.swift b/Sources/AppKitBackend/AppKitBackend.swift index 1d7fcb58af..ece14a9db3 100644 --- a/Sources/AppKitBackend/AppKitBackend.swift +++ b/Sources/AppKitBackend/AppKitBackend.swift @@ -1708,8 +1708,8 @@ public final class AppKitBackend: AppBackend { ) { let contentSize = naturalSize(of: content) - let width = max(contentSize.x, 80) - let height = max(contentSize.y, 80) + let width = max(contentSize.x, 10) + let height = max(contentSize.y, 10) sheet.setContentSize(NSSize(width: width, height: height)) sheet.contentView = content @@ -1734,6 +1734,38 @@ public final class AppKitBackend: AppBackend { NSApplication.shared.stopModal() } } + + public func setPresentationBackground(of sheet: NSCustomSheet, to color: Color) { + let backgroundView = NSView() + backgroundView.wantsLayer = true + backgroundView.layer?.backgroundColor = color.nsColor.cgColor + + if let existingContentView = sheet.contentView { + let container = NSView() + container.translatesAutoresizingMaskIntoConstraints = false + + container.addSubview(backgroundView) + backgroundView.translatesAutoresizingMaskIntoConstraints = false + backgroundView.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = + true + backgroundView.topAnchor.constraint(equalTo: container.topAnchor).isActive = true + backgroundView.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = + true + backgroundView.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = true + + container.addSubview(existingContentView) + existingContentView.translatesAutoresizingMaskIntoConstraints = false + existingContentView.leadingAnchor.constraint(equalTo: container.leadingAnchor) + .isActive = true + existingContentView.topAnchor.constraint(equalTo: container.topAnchor).isActive = true + existingContentView.trailingAnchor.constraint(equalTo: container.trailingAnchor) + .isActive = true + existingContentView.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = + true + + sheet.contentView = container + } + } } public final class NSCustomSheet: NSCustomWindow, NSWindowDelegate, SheetImplementation { diff --git a/Sources/SwiftCrossUI/Backend/AppBackend.swift b/Sources/SwiftCrossUI/Backend/AppBackend.swift index 6f587700c7..bc786d17f5 100644 --- a/Sources/SwiftCrossUI/Backend/AppBackend.swift +++ b/Sources/SwiftCrossUI/Backend/AppBackend.swift @@ -654,8 +654,28 @@ public protocol AppBackend: Sendable { /// - detents: An array of detents that the sheet can be resized to. func setPresentationDetents(of sheet: Sheet, to detents: [PresentationDetent]) + /// Sets the visibility for a sheet presentation. + /// + /// This method is called when the sheet content has a `presentationDragIndicatorVisibility` + /// modifier applied at its top level. + /// + /// - Parameters: + /// - sheet: The sheet to apply the detents to. + /// - visibility: visibility of the drag indicator (visible or hidden) func setPresentationDragIndicatorVisibility( - of sheet: Sheet, to visibility: PresentationDragIndicatorVisibility) + of sheet: Sheet, + to visibility: PresentationDragIndicatorVisibility + ) + + /// Sets the background color for a sheet presentation. + /// + /// This method is called when the sheet content has a `presentationBackground` + /// modifier applied at its top level. + /// + /// - Parameters: + /// - sheet: The sheet to apply the detents to. + /// - color: rgba background color + func setPresentationBackground(of sheet: Sheet, to color: Color) /// Presents an 'Open file' dialog to the user for selecting files or /// folders. @@ -786,9 +806,11 @@ extension AppBackend { } private func ignored(_ function: String = #function) { - print( - "\(type(of: self)): \(function) is being ignored\nConsult at the documentation for further information." - ) + #if DEBUG + print( + "\(type(of: self)): \(function) is being ignored\nConsult at the documentation for further information." + ) + #endif } // MARK: System @@ -1263,4 +1285,8 @@ extension AppBackend { ) { ignored() } + + func setPresentationBackground(of sheet: Sheet, to color: Color) { + todo() + } } diff --git a/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift b/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift index 519b6b59d2..bcc2eef6ca 100644 --- a/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift +++ b/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift @@ -4,7 +4,9 @@ public struct PreferenceValues: Sendable { public static let `default` = PreferenceValues( onOpenURL: nil, presentationDetents: nil, - presentationCornerRadius: nil + presentationCornerRadius: nil, + presentationDragIndicatorVisibility: nil, + presentationBackground: nil ) public var onOpenURL: (@Sendable @MainActor (URL) -> Void)? @@ -15,17 +17,23 @@ public struct PreferenceValues: Sendable { /// The corner radius for a sheet presentation. Only applies to the top-level view in a sheet. public var presentationCornerRadius: Double? + /// The drag indicator visibiity for a sheet presentation. Only applies to the top-level view in a sheet. public var presentationDragIndicatorVisibility: PresentationDragIndicatorVisibility? + public var presentationBackground: Color? + public init( onOpenURL: (@Sendable @MainActor (URL) -> Void)?, presentationDetents: [PresentationDetent]? = nil, presentationCornerRadius: Double? = nil, - presentationDragIndicatorVisibility: PresentationDragIndicatorVisibility? = nil + presentationDragIndicatorVisibility: PresentationDragIndicatorVisibility? = nil, + presentationBackground: Color? ) { self.onOpenURL = onOpenURL self.presentationDetents = presentationDetents self.presentationCornerRadius = presentationCornerRadius + self.presentationDragIndicatorVisibility = presentationDragIndicatorVisibility + self.presentationBackground = presentationBackground } public init(merging children: [PreferenceValues]) { @@ -44,5 +52,6 @@ public struct PreferenceValues: Sendable { presentationDetents = children.first?.presentationDetents presentationCornerRadius = children.first?.presentationCornerRadius presentationDragIndicatorVisibility = children.first?.presentationDragIndicatorVisibility + presentationBackground = children.first?.presentationBackground } } diff --git a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift index 0e36ffb125..ac5b209902 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift @@ -111,6 +111,10 @@ struct SheetModifier: TypeSafeView { of: sheet, to: presentationDragIndicatorVisibility) } + if let presentationBackground = preferences.presentationBackground { + backend.setPresentationBackground(of: sheet, to: presentationBackground) + } + backend.showSheet( sheet, window: .some(environment.window! as! Backend.Window) diff --git a/Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationModifiers.swift b/Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationModifiers.swift index 512095b0f7..47f8be56b6 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationModifiers.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationModifiers.swift @@ -34,4 +34,8 @@ extension View { ) -> some View { preference(key: \.presentationDragIndicatorVisibility, value: visibility) } + + public func presentationBackground(_ color: Color) -> some View { + preference(key: \.presentationBackground, value: color) + } } diff --git a/Sources/UIKitBackend/UIKitBackend+Sheet.swift b/Sources/UIKitBackend/UIKitBackend+Sheet.swift index a13de8e97b..65f1ff0947 100644 --- a/Sources/UIKitBackend/UIKitBackend+Sheet.swift +++ b/Sources/UIKitBackend/UIKitBackend+Sheet.swift @@ -97,6 +97,10 @@ extension UIKitBackend { #endif } } + + public func setPresentationBackground(of sheet: CustomSheet, to color: Color) { + sheet.view.backgroundColor = color.uiColor + } } public final class CustomSheet: UIViewController, SheetImplementation { From 3108f7734c171447630b4090b1276f208ed2d04e Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Fri, 3 Oct 2025 22:22:20 +0200 Subject: [PATCH 07/27] 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 --- .../WindowingExample/WindowingApp.swift | 43 +++++++++++- .../Environment/Actions/DismissAction.swift | 70 +++++++++++++++++++ .../PresentationDragIndicatorVisibility.swift | 2 +- .../Views/Modifiers/SheetModifier.swift | 11 ++- Sources/UIKitBackend/UIKitBackend+Sheet.swift | 24 ++++++- 5 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 Sources/SwiftCrossUI/Environment/Actions/DismissAction.swift diff --git a/Examples/Sources/WindowingExample/WindowingApp.swift b/Examples/Sources/WindowingExample/WindowingApp.swift index f878c3451d..a589167876 100644 --- a/Examples/Sources/WindowingExample/WindowingApp.swift +++ b/Examples/Sources/WindowingExample/WindowingApp.swift @@ -64,6 +64,7 @@ struct AlertDemo: View { } } +// kind of a stress test for the dismiss action struct SheetDemo: View { @State var isPresented = false @State var isShortTermSheetPresented = false @@ -85,7 +86,7 @@ struct SheetDemo: View { SheetBody() .presentationDetents([.height(150), .medium, .large]) .presentationDragIndicatorVisibility(.visible) - .presentationBackground(.blue) + .presentationBackground(.green) } .sheet(isPresented: $isShortTermSheetPresented) { Text("I'm only here for 5s") @@ -98,6 +99,7 @@ struct SheetDemo: View { struct SheetBody: View { @State var isPresented = false + @Environment(\.dismiss) var dismiss var body: some View { VStack { @@ -107,12 +109,51 @@ struct SheetDemo: View { isPresented = true print("should get presented") } + Button("Dismiss") { + dismiss() + } Spacer() } .sheet(isPresented: $isPresented) { print("nested sheet dismissed") } content: { + NestedSheetBody(dismissParent: { dismiss() }) + } + } + + struct NestedSheetBody: View { + @Environment(\.dismiss) var dismiss + var dismissParent: () -> Void + @State var showNextChild = false + + var body: some View { Text("I'm nested. Its claustrophobic in here.") + Button("New Child Sheet") { + showNextChild = true + } + .sheet(isPresented: $showNextChild) { + DoubleNestedSheetBody(dismissParent: { dismiss() }) + } + Button("dismiss parent sheet") { + dismissParent() + } + Button("dismiss") { + dismiss() + } + } + } + struct DoubleNestedSheetBody: View { + @Environment(\.dismiss) var dismiss + var dismissParent: () -> Void + + var body: some View { + Text("I'm nested. Its claustrophobic in here.") + Button("dismiss parent sheet") { + dismissParent() + } + Button("dismiss") { + dismiss() + } } } } diff --git a/Sources/SwiftCrossUI/Environment/Actions/DismissAction.swift b/Sources/SwiftCrossUI/Environment/Actions/DismissAction.swift new file mode 100644 index 0000000000..1258d67193 --- /dev/null +++ b/Sources/SwiftCrossUI/Environment/Actions/DismissAction.swift @@ -0,0 +1,70 @@ +/// An action that dismisses the current presentation context. +/// +/// Use the `dismiss` environment value to get an instance of this action, +/// then call it to dismiss the current sheet. +/// +/// Example usage: +/// ```swift +/// struct SheetContentView: View { +/// @Environment(\.dismiss) var dismiss +/// +/// var body: some View { +/// VStack { +/// Text("Sheet Content") +/// Button("Close") { +/// dismiss() +/// } +/// } +/// } +/// } +/// ``` +@MainActor +public struct DismissAction { + private let action: () -> Void + + internal init(action: @escaping () -> Void) { + self.action = action + } + + /// Dismisses the current presentation context. + public func callAsFunction() { + action() + } +} + +/// Environment key for the dismiss action. +private struct DismissActionKey: EnvironmentKey { + @MainActor + static var defaultValue: DismissAction { + DismissAction(action: { + #if DEBUG + print("warning: dismiss() called but no presentation context is available") + #endif + }) + } +} + +extension EnvironmentValues { + /// An action that dismisses the current presentation context. + /// + /// Use this environment value to get a dismiss action that can be called + /// to dismiss the current sheet, popover, or other presentation. + /// + /// Example: + /// ```swift + /// struct ContentView: View { + /// @Environment(\.dismiss) var dismiss + /// + /// var body: some View { + /// Button("Close") { + /// dismiss() + /// } + /// } + /// } + /// ``` + @MainActor + public var dismiss: DismissAction { + get { self[DismissActionKey.self] } + set { self[DismissActionKey.self] = newValue } + } +} diff --git a/Sources/SwiftCrossUI/Values/PresentationDragIndicatorVisibility.swift b/Sources/SwiftCrossUI/Values/PresentationDragIndicatorVisibility.swift index 60151bff6a..da2cd54978 100644 --- a/Sources/SwiftCrossUI/Values/PresentationDragIndicatorVisibility.swift +++ b/Sources/SwiftCrossUI/Values/PresentationDragIndicatorVisibility.swift @@ -1,3 +1,3 @@ -public enum PresentationDragIndicatorVisibility { +public enum PresentationDragIndicatorVisibility: Sendable { case hidden, visible } diff --git a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift index ac5b209902..b219654e79 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift @@ -73,10 +73,15 @@ struct SheetModifier: TypeSafeView { if isPresented.wrappedValue && children.sheet == nil { let sheet = backend.createSheet() + let dismissAction = DismissAction(action: { [isPresented] in + isPresented.wrappedValue = false + }) + let sheetEnvironment = environment.with(\.dismiss, dismissAction) + let dryRunResult = children.sheetContentNode.update( with: sheetContent(), proposedSize: sheet.size, - environment: environment, + environment: sheetEnvironment, dryRun: true ) @@ -85,7 +90,7 @@ struct SheetModifier: TypeSafeView { let _ = children.sheetContentNode.update( with: sheetContent(), proposedSize: sheet.size, - environment: environment, + environment: sheetEnvironment, dryRun: false ) @@ -95,7 +100,7 @@ struct SheetModifier: TypeSafeView { onDismiss: handleDismiss ) - // Apply presentation preferences to the sheet + // MARK: Sheet Presentation Preferences if let cornerRadius = preferences.presentationCornerRadius { backend.setPresentationCornerRadius(of: sheet, to: cornerRadius) } diff --git a/Sources/UIKitBackend/UIKitBackend+Sheet.swift b/Sources/UIKitBackend/UIKitBackend+Sheet.swift index 65f1ff0947..6b600e2c94 100644 --- a/Sources/UIKitBackend/UIKitBackend+Sheet.swift +++ b/Sources/UIKitBackend/UIKitBackend+Sheet.swift @@ -26,7 +26,15 @@ extension UIKitBackend { } public func dismissSheet(_ sheet: CustomSheet, window: UIWindow?) { - sheet.dismiss(animated: true) + // If this sheet has a presented view controller (nested sheet), dismiss it first + if let presentedVC = sheet.presentedViewController { + presentedVC.dismiss(animated: false) { [weak sheet] in + // After the nested sheet is dismissed, dismiss this sheet + sheet?.dismissProgrammatically() + } + } else { + sheet.dismissProgrammatically() + } } public func setPresentationDetents(of sheet: CustomSheet, to detents: [PresentationDetent]) { @@ -110,12 +118,24 @@ public final class CustomSheet: UIViewController, SheetImplementation { } var onDismiss: (() -> Void)? + private var isDismissedProgrammatically = false public override func viewDidLoad() { super.viewDidLoad() } + func dismissProgrammatically() { + isDismissedProgrammatically = true + dismiss(animated: true) + } + public override func viewDidDisappear(_ animated: Bool) { - onDismiss?() + super.viewDidDisappear(animated) + + // Only call onDismiss if the sheet was dismissed by user interaction (swipe down, tap outside) + // not when dismissed programmatically via the dismiss action + if !isDismissedProgrammatically { + onDismiss?() + } } } From 7df381c78723ce2a95118987dcb3200d5d576e7e Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Sat, 4 Oct 2025 14:38:07 +0200 Subject: [PATCH 08/27] added interactiveDismissDisabled modifier --- .../Sources/WindowingExample/WindowingApp.swift | 1 + Sources/AppKitBackend/AppKitBackend.swift | 10 +++++++++- Sources/SwiftCrossUI/Backend/AppBackend.swift | 16 ++++++++++++++++ .../ViewGraph/PreferenceValues.swift | 13 ++++++++++--- .../{Style => }/PresentationModifiers.swift | 4 ++++ .../Views/Modifiers/SheetModifier.swift | 4 ++++ Sources/UIKitBackend/UIKitBackend+Sheet.swift | 4 ++++ 7 files changed, 48 insertions(+), 4 deletions(-) rename Sources/SwiftCrossUI/Views/Modifiers/{Style => }/PresentationModifiers.swift (91%) diff --git a/Examples/Sources/WindowingExample/WindowingApp.swift b/Examples/Sources/WindowingExample/WindowingApp.swift index a589167876..e621f5ff4e 100644 --- a/Examples/Sources/WindowingExample/WindowingApp.swift +++ b/Examples/Sources/WindowingExample/WindowingApp.swift @@ -133,6 +133,7 @@ struct SheetDemo: View { } .sheet(isPresented: $showNextChild) { DoubleNestedSheetBody(dismissParent: { dismiss() }) + .interactiveDismissDisabled() } Button("dismiss parent sheet") { dismissParent() diff --git a/Sources/AppKitBackend/AppKitBackend.swift b/Sources/AppKitBackend/AppKitBackend.swift index ece14a9db3..c28ca6fd01 100644 --- a/Sources/AppKitBackend/AppKitBackend.swift +++ b/Sources/AppKitBackend/AppKitBackend.swift @@ -1766,6 +1766,10 @@ public final class AppKitBackend: AppBackend { sheet.contentView = container } } + + public func setInteractiveDismissDisabled(for sheet: NSCustomSheet, to disabled: Bool) { + sheet.interactiveDismissDisabled = disabled + } } public final class NSCustomSheet: NSCustomWindow, NSWindowDelegate, SheetImplementation { @@ -1777,13 +1781,17 @@ public final class NSCustomSheet: NSCustomWindow, NSWindowDelegate, SheetImpleme } public var onDismiss: (() -> Void)? + public var interactiveDismissDisabled: Bool = false + public func dismiss() { onDismiss?() self.contentViewController?.dismiss(self) } @objc override public func cancelOperation(_ sender: Any?) { - dismiss() + if !interactiveDismissDisabled { + dismiss() + } } } diff --git a/Sources/SwiftCrossUI/Backend/AppBackend.swift b/Sources/SwiftCrossUI/Backend/AppBackend.swift index bc786d17f5..9123ec9f49 100644 --- a/Sources/SwiftCrossUI/Backend/AppBackend.swift +++ b/Sources/SwiftCrossUI/Backend/AppBackend.swift @@ -677,6 +677,18 @@ public protocol AppBackend: Sendable { /// - color: rgba background color func setPresentationBackground(of sheet: Sheet, to color: Color) + /// Sets the interactive dismissablility of a sheet. + /// when disabled the sheet can only be closed programmatically, + /// not through users swiping, escape keys or similar. + /// + /// This method is called when the sheet content has a `interactiveDismissDisabled` + /// modifier applied at its top level. + /// + /// - Parameters: + /// - sheet: The sheet to apply the detents to. + /// - disabled: wether its disabled + func setInteractiveDismissDisabled(for sheet: Sheet, to disabled: Bool) + /// Presents an 'Open file' dialog to the user for selecting files or /// folders. /// @@ -1289,4 +1301,8 @@ extension AppBackend { func setPresentationBackground(of sheet: Sheet, to color: Color) { todo() } + + func setInteractiveDismissDisabled(for sheet: Sheet, to disabled: Bool) { + todo() + } } diff --git a/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift b/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift index bcc2eef6ca..df0e0f446f 100644 --- a/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift +++ b/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift @@ -6,7 +6,8 @@ public struct PreferenceValues: Sendable { presentationDetents: nil, presentationCornerRadius: nil, presentationDragIndicatorVisibility: nil, - presentationBackground: nil + presentationBackground: nil, + interactiveDismissDisabled: nil ) public var onOpenURL: (@Sendable @MainActor (URL) -> Void)? @@ -17,23 +18,28 @@ public struct PreferenceValues: Sendable { /// The corner radius for a sheet presentation. Only applies to the top-level view in a sheet. public var presentationCornerRadius: Double? - /// The drag indicator visibiity for a sheet presentation. Only applies to the top-level view in a sheet. + /// The drag indicator visibility for a sheet presentation. Only applies to the top-level view in a sheet. public var presentationDragIndicatorVisibility: PresentationDragIndicatorVisibility? + /// The backgroundcolor of a sheet. Only applies to the top-level view in a sheet public var presentationBackground: Color? + public var interactiveDismissDisabled: Bool? + public init( onOpenURL: (@Sendable @MainActor (URL) -> Void)?, presentationDetents: [PresentationDetent]? = nil, presentationCornerRadius: Double? = nil, presentationDragIndicatorVisibility: PresentationDragIndicatorVisibility? = nil, - presentationBackground: Color? + presentationBackground: Color? = nil, + interactiveDismissDisabled: Bool? = nil ) { self.onOpenURL = onOpenURL self.presentationDetents = presentationDetents self.presentationCornerRadius = presentationCornerRadius self.presentationDragIndicatorVisibility = presentationDragIndicatorVisibility self.presentationBackground = presentationBackground + self.interactiveDismissDisabled = interactiveDismissDisabled } public init(merging children: [PreferenceValues]) { @@ -53,5 +59,6 @@ public struct PreferenceValues: Sendable { presentationCornerRadius = children.first?.presentationCornerRadius presentationDragIndicatorVisibility = children.first?.presentationDragIndicatorVisibility presentationBackground = children.first?.presentationBackground + interactiveDismissDisabled = children.first?.interactiveDismissDisabled } } diff --git a/Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationModifiers.swift b/Sources/SwiftCrossUI/Views/Modifiers/PresentationModifiers.swift similarity index 91% rename from Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationModifiers.swift rename to Sources/SwiftCrossUI/Views/Modifiers/PresentationModifiers.swift index 47f8be56b6..0a45edaaec 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/Style/PresentationModifiers.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/PresentationModifiers.swift @@ -38,4 +38,8 @@ extension View { public func presentationBackground(_ color: Color) -> some View { preference(key: \.presentationBackground, value: color) } + + public func interactiveDismissDisabled(_ isDisabled: Bool = true) -> some View { + preference(key: \.interactiveDismissDisabled, value: isDisabled) + } } diff --git a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift index b219654e79..d2fd8ecb84 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift @@ -120,6 +120,10 @@ struct SheetModifier: TypeSafeView { backend.setPresentationBackground(of: sheet, to: presentationBackground) } + if let interactiveDismissDisabled = preferences.interactiveDismissDisabled { + backend.setInteractiveDismissDisabled(for: sheet, to: interactiveDismissDisabled) + } + backend.showSheet( sheet, window: .some(environment.window! as! Backend.Window) diff --git a/Sources/UIKitBackend/UIKitBackend+Sheet.swift b/Sources/UIKitBackend/UIKitBackend+Sheet.swift index 6b600e2c94..6cef72b4ac 100644 --- a/Sources/UIKitBackend/UIKitBackend+Sheet.swift +++ b/Sources/UIKitBackend/UIKitBackend+Sheet.swift @@ -109,6 +109,10 @@ extension UIKitBackend { public func setPresentationBackground(of sheet: CustomSheet, to color: Color) { sheet.view.backgroundColor = color.uiColor } + + public func setInteractiveDismissDisabled(for sheet: Sheet, to disabled: Bool) { + sheet.isModalInPresentation = disabled + } } public final class CustomSheet: UIViewController, SheetImplementation { From 8669cb6b03724fa94c95978d10341f49d255e40e Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Wed, 8 Oct 2025 16:13:09 +0200 Subject: [PATCH 09/27] renamed size Parameter of SheetImplementation Protocol to sheetSize --- Sources/AppKitBackend/AppKitBackend.swift | 2 +- Sources/SwiftCrossUI/Backend/AppBackend.swift | 2 +- Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift | 4 ++-- Sources/UIKitBackend/UIKitBackend+Sheet.swift | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/AppKitBackend/AppKitBackend.swift b/Sources/AppKitBackend/AppKitBackend.swift index c28ca6fd01..af49464896 100644 --- a/Sources/AppKitBackend/AppKitBackend.swift +++ b/Sources/AppKitBackend/AppKitBackend.swift @@ -1773,7 +1773,7 @@ public final class AppKitBackend: AppBackend { } public final class NSCustomSheet: NSCustomWindow, NSWindowDelegate, SheetImplementation { - public var size: SIMD2 { + public var sheetSize: SIMD2 { guard let size = self.contentView?.frame.size else { return SIMD2(x: 0, y: 0) } diff --git a/Sources/SwiftCrossUI/Backend/AppBackend.swift b/Sources/SwiftCrossUI/Backend/AppBackend.swift index 9123ec9f49..919f341562 100644 --- a/Sources/SwiftCrossUI/Backend/AppBackend.swift +++ b/Sources/SwiftCrossUI/Backend/AppBackend.swift @@ -807,7 +807,7 @@ extension AppBackend { } public protocol SheetImplementation { - var size: SIMD2 { get } + var sheetSize: SIMD2 { get } } extension AppBackend { diff --git a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift index d2fd8ecb84..8e2e0bd710 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift @@ -80,7 +80,7 @@ struct SheetModifier: TypeSafeView { let dryRunResult = children.sheetContentNode.update( with: sheetContent(), - proposedSize: sheet.size, + proposedSize: sheet.sheetSize, environment: sheetEnvironment, dryRun: true ) @@ -89,7 +89,7 @@ struct SheetModifier: TypeSafeView { let _ = children.sheetContentNode.update( with: sheetContent(), - proposedSize: sheet.size, + proposedSize: sheet.sheetSize, environment: sheetEnvironment, dryRun: false ) diff --git a/Sources/UIKitBackend/UIKitBackend+Sheet.swift b/Sources/UIKitBackend/UIKitBackend+Sheet.swift index 6cef72b4ac..a0c265e5b9 100644 --- a/Sources/UIKitBackend/UIKitBackend+Sheet.swift +++ b/Sources/UIKitBackend/UIKitBackend+Sheet.swift @@ -116,7 +116,7 @@ extension UIKitBackend { } public final class CustomSheet: UIViewController, SheetImplementation { - public var size: SIMD2 { + public var sheetSize: SIMD2 { let size = view.frame.size return SIMD2(x: Int(size.width), y: Int(size.height)) } From ed300d2d0bbc9a7ed2d6967df81f5f40d62c1c56 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Wed, 8 Oct 2025 17:13:53 +0200 Subject: [PATCH 10/27] GtkBackend Sheets save --- Sources/GtkBackend/GtkBackend.swift | 110 ++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/Sources/GtkBackend/GtkBackend.swift b/Sources/GtkBackend/GtkBackend.swift index 5bc07226c1..d0a44f17ae 100644 --- a/Sources/GtkBackend/GtkBackend.swift +++ b/Sources/GtkBackend/GtkBackend.swift @@ -22,6 +22,7 @@ public final class GtkBackend: AppBackend { public typealias Widget = Gtk.Widget public typealias Menu = Gtk.PopoverMenu public typealias Alert = Gtk.MessageDialog + public typealias Sheet = Gtk.Window public final class Path { var path: SwiftCrossUI.Path? @@ -48,6 +49,45 @@ public final class GtkBackend: AppBackend { /// precreated window until it gets 'created' via `createWindow`. var windows: [Window] = [] + // Sheet management (close-request, programmatic dismiss, interactive lock) + private final class SheetContext { + var onDismiss: () -> Void + var isProgrammaticDismiss: Bool = false + var interactiveDismissDisabled: Bool = false + + init(onDismiss: @escaping () -> Void) { + self.onDismiss = onDismiss + } + } + + private var sheetContexts: [OpaquePointer: SheetContext] = [:] + private var connectedCloseHandlers: Set = [] + + // C thunk for GtkWindow::close-request + private static let closeRequestThunk: + @convention(c) ( + UnsafeMutableRawPointer?, UnsafeMutableRawPointer? + ) -> Int32 = { instance, userData in + // TRUE (1) = consume event (prevent native close) + guard let instance, let userData else { return 1 } + let backend = Unmanaged.fromOpaque(userData).takeUnretainedValue() + let key = OpaquePointer(instance) + guard let ctx = backend.sheetContexts[key] else { return 1 } + + if ctx.interactiveDismissDisabled { return 1 } + + if ctx.isProgrammaticDismiss { + // Suppress onDismiss for programmatic closes + ctx.isProgrammaticDismiss = false + return 1 + } + + backend.runInMainThread { + ctx.onDismiss() + } + return 1 + } + // A separate initializer to satisfy ``AppBackend``'s requirements. public convenience init() { self.init(appIdentifier: nil) @@ -1569,6 +1609,70 @@ public final class GtkBackend: AppBackend { return properties } + public func createSheet() -> Gtk.Window { + return Gtk.Window() + } + + public func updateSheet(_ sheet: Gtk.Window, content: Widget, onDismiss: @escaping () -> Void) { + sheet.setChild(content) + + // Track per-sheet context and hook close-request once + let key: OpaquePointer = OpaquePointer(sheet.widgetPointer) + + if let ctx = sheetContexts[key] { + // Update onDismiss if sheet already tracked + ctx.onDismiss = onDismiss + } else { + // First-time setup: store context and connect signal + let ctx = SheetContext(onDismiss: onDismiss) + sheetContexts[key] = ctx + + if connectedCloseHandlers.insert(key).inserted { + let handler: GCallback = unsafeBitCast(Self.closeRequestThunk, to: GCallback.self) + g_signal_connect_data( + UnsafeMutableRawPointer(sheet.gobjectPointer), + "close-request", + handler, + Unmanaged.passUnretained(self).toOpaque(), + nil, + GConnectFlags(0) + ) + } + } + } + + public func showSheet(_ sheet: Gtk.Window, window: ApplicationWindow?) { + sheet.isModal = true + sheet.isDecorated = false // optional for a more sheet-like look + sheet.setTransient(for: window ?? windows[0]) + sheet.present() + } + + public func dismissSheet(_ sheet: Gtk.Window, window: ApplicationWindow?) { + let key: OpaquePointer = OpaquePointer(sheet.widgetPointer) + if let ctx = sheetContexts[key] { + // Suppress onDismiss when closing programmatically + ctx.isProgrammaticDismiss = true + } + sheet.destroy() + sheetContexts.removeValue(forKey: key) + connectedCloseHandlers.remove(key) + } + + public func setPresentationBackground(of sheet: Gtk.Window, to color: SwiftCrossUI.Color) { + sheet.css.set(properties: [.backgroundColor(color.gtkColor)]) + } + + public func setInteractiveDismissDisabled(for sheet: Gtk.Window, to disabled: Bool) { + let key: OpaquePointer = OpaquePointer(sheet.widgetPointer) + if let ctx = sheetContexts[key] { + ctx.interactiveDismissDisabled = disabled + } else { + let ctx = SheetContext(onDismiss: {}) + ctx.interactiveDismissDisabled = disabled + sheetContexts[key] = ctx + } + } } extension UnsafeMutablePointer { @@ -1581,3 +1685,9 @@ extension UnsafeMutablePointer { class CustomListBox: ListBox { var cachedSelection: Int? = nil } + +extension Gtk.Window: SheetImplementation { + public var sheetSize: SIMD2 { + return SIMD2(x: self.size.width, y: self.size.height) + } +} From a76ca7c179bee3fb3c663d302889dbbb53448339 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Thu, 9 Oct 2025 00:59:22 +0200 Subject: [PATCH 11/27] finished GtkBackendSheets --- .../WindowingExample/WindowingApp.swift | 1 + Package.resolved | 2 +- Sources/GtkBackend/GtkBackend.swift | 106 +++++++++++++----- 3 files changed, 81 insertions(+), 28 deletions(-) diff --git a/Examples/Sources/WindowingExample/WindowingApp.swift b/Examples/Sources/WindowingExample/WindowingApp.swift index e621f5ff4e..d0a79afc83 100644 --- a/Examples/Sources/WindowingExample/WindowingApp.swift +++ b/Examples/Sources/WindowingExample/WindowingApp.swift @@ -118,6 +118,7 @@ struct SheetDemo: View { print("nested sheet dismissed") } content: { NestedSheetBody(dismissParent: { dismiss() }) + .presentationCornerRadius(35) } } diff --git a/Package.resolved b/Package.resolved index 18390df54a..13507586f2 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "589b3dd67c6c4bf002ac0e661cdc5f048304c975897d3542f1623910c0b856d2", + "originHash" : "2ce783f3e8fad62599b6c6d22660ffc4e6abf55121ba292835278e9377b1f871", "pins" : [ { "identity" : "jpeg", diff --git a/Sources/GtkBackend/GtkBackend.swift b/Sources/GtkBackend/GtkBackend.swift index d0a44f17ae..28e411e36c 100644 --- a/Sources/GtkBackend/GtkBackend.swift +++ b/Sources/GtkBackend/GtkBackend.swift @@ -37,6 +37,7 @@ public final class GtkBackend: AppBackend { public let menuImplementationStyle = MenuImplementationStyle.dynamicPopover public let canRevealFiles = true public let deviceClass = DeviceClass.desktop + public let defaultSheetCornerRadius = 10 var gtkApp: Application @@ -73,21 +74,35 @@ public final class GtkBackend: AppBackend { let backend = Unmanaged.fromOpaque(userData).takeUnretainedValue() let key = OpaquePointer(instance) guard let ctx = backend.sheetContexts[key] else { return 1 } - + if ctx.interactiveDismissDisabled { return 1 } - + if ctx.isProgrammaticDismiss { // Suppress onDismiss for programmatic closes ctx.isProgrammaticDismiss = false return 1 } - + backend.runInMainThread { ctx.onDismiss() } return 1 } - + + // C-convention thunk for key-pressed + private let escapeKeyPressedThunk: @convention(c) ( + UnsafeMutableRawPointer?, guint, guint, GdkModifierType, gpointer? + ) -> gboolean = { controller, keyval, keycode, state, userData in + // TRUE (1) = consume event + if keyval == GDK_KEY_Escape { + guard let userData else { return 1 } + let box = Unmanaged Void>>.fromOpaque(userData).takeUnretainedValue() + box.value() + return 1 // consume + } + return 0 // let others handle + } + // A separate initializer to satisfy ``AppBackend``'s requirements. public convenience init() { self.init(appIdentifier: nil) @@ -1616,34 +1631,53 @@ public final class GtkBackend: AppBackend { public func updateSheet(_ sheet: Gtk.Window, content: Widget, onDismiss: @escaping () -> Void) { sheet.setChild(content) - // Track per-sheet context and hook close-request once let key: OpaquePointer = OpaquePointer(sheet.widgetPointer) - if let ctx = sheetContexts[key] { - // Update onDismiss if sheet already tracked - ctx.onDismiss = onDismiss - } else { - // First-time setup: store context and connect signal - let ctx = SheetContext(onDismiss: onDismiss) - sheetContexts[key] = ctx - - if connectedCloseHandlers.insert(key).inserted { - let handler: GCallback = unsafeBitCast(Self.closeRequestThunk, to: GCallback.self) - g_signal_connect_data( - UnsafeMutableRawPointer(sheet.gobjectPointer), - "close-request", - handler, - Unmanaged.passUnretained(self).toOpaque(), - nil, - GConnectFlags(0) - ) - } + //add a slight border to not be just a flat corner + sheet.css.set(property: .border(color: SwiftCrossUI.Color.gray.gtkColor, width: 1)) + + let ctx = getOrCreateSheetContext(for: sheet) + ctx.onDismiss = onDismiss + + sheet.css.set(property: .cornerRadius(defaultSheetCornerRadius)) + + if connectedCloseHandlers.insert(key).inserted { + let handler: GCallback = unsafeBitCast(Self.closeRequestThunk, to: GCallback.self) + g_signal_connect_data( + UnsafeMutableRawPointer(sheet.gobjectPointer), + "close-request", + handler, + Unmanaged.passUnretained(self).toOpaque(), + nil, + GConnectFlags(0) + ) + + let escapeHandler = gtk_event_controller_key_new() + gtk_event_controller_set_propagation_phase(escapeHandler, GTK_PHASE_BUBBLE) + g_signal_connect_data ( + UnsafeMutableRawPointer(escapeHandler), + "key-pressed", + unsafeBitCast(escapeKeyPressedThunk, to: GCallback.self), + Unmanaged.passRetained(ValueBox(value: { + if ctx.interactiveDismissDisabled { return } + self.runInMainThread { + ctx.onDismiss() + } + })).toOpaque(), + { data, _ in + if let data { + Unmanaged Void>>.fromOpaque(data).release() + } + }, + G_CONNECT_DEFAULT + ) + gtk_widget_add_controller(sheet.widgetPointer, escapeHandler) } } public func showSheet(_ sheet: Gtk.Window, window: ApplicationWindow?) { sheet.isModal = true - sheet.isDecorated = false // optional for a more sheet-like look + sheet.isDecorated = false sheet.setTransient(for: window ?? windows[0]) sheet.present() } @@ -1664,13 +1698,24 @@ public final class GtkBackend: AppBackend { } public func setInteractiveDismissDisabled(for sheet: Gtk.Window, to disabled: Bool) { + let ctx = getOrCreateSheetContext(for: sheet) + + ctx.interactiveDismissDisabled = disabled + } + + public func setPresentationCornerRadius(of sheet: Gtk.Window, to radius: Double) { + let radius = Int(radius) + sheet.css.set(property: .cornerRadius(radius)) + } + + private func getOrCreateSheetContext(for sheet: Gtk.Window) -> SheetContext { let key: OpaquePointer = OpaquePointer(sheet.widgetPointer) if let ctx = sheetContexts[key] { - ctx.interactiveDismissDisabled = disabled + return ctx } else { let ctx = SheetContext(onDismiss: {}) - ctx.interactiveDismissDisabled = disabled sheetContexts[key] = ctx + return ctx } } } @@ -1691,3 +1736,10 @@ extension Gtk.Window: SheetImplementation { return SIMD2(x: self.size.width, y: self.size.height) } } + +final class ValueBox { + let value: T + init(value: T) { + self.value = value + } +} From 941131e8c00bc9efd6931975e3201d0b03c28209 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Thu, 9 Oct 2025 01:14:41 +0200 Subject: [PATCH 12/27] documentation improvements --- Sources/GtkBackend/GtkBackend.swift | 6 ++-- .../Modifiers/PresentationModifiers.swift | 32 ++++++++++++++++--- .../Views/Modifiers/SheetModifier.swift | 2 +- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/Sources/GtkBackend/GtkBackend.swift b/Sources/GtkBackend/GtkBackend.swift index 28e411e36c..30f4ace11f 100644 --- a/Sources/GtkBackend/GtkBackend.swift +++ b/Sources/GtkBackend/GtkBackend.swift @@ -78,7 +78,6 @@ public final class GtkBackend: AppBackend { if ctx.interactiveDismissDisabled { return 1 } if ctx.isProgrammaticDismiss { - // Suppress onDismiss for programmatic closes ctx.isProgrammaticDismiss = false return 1 } @@ -98,9 +97,9 @@ public final class GtkBackend: AppBackend { guard let userData else { return 1 } let box = Unmanaged Void>>.fromOpaque(userData).takeUnretainedValue() box.value() - return 1 // consume + return 1 } - return 0 // let others handle + return 0 } // A separate initializer to satisfy ``AppBackend``'s requirements. @@ -1685,7 +1684,6 @@ public final class GtkBackend: AppBackend { public func dismissSheet(_ sheet: Gtk.Window, window: ApplicationWindow?) { let key: OpaquePointer = OpaquePointer(sheet.widgetPointer) if let ctx = sheetContexts[key] { - // Suppress onDismiss when closing programmatically ctx.isProgrammaticDismiss = true } sheet.destroy() diff --git a/Sources/SwiftCrossUI/Views/Modifiers/PresentationModifiers.swift b/Sources/SwiftCrossUI/Views/Modifiers/PresentationModifiers.swift index 0a45edaaec..f2ffb0b99a 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/PresentationModifiers.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/PresentationModifiers.swift @@ -20,25 +20,47 @@ extension View { /// This modifier only affects the sheet presentation itself when applied to the /// top-level view within a sheet. It does not affect the content's corner radius. /// - /// supported platforms: iOS (ignored on unsupported platforms) - /// ignored on: older than iOS 15 + /// supported platforms: iOS 15+, Gtk4 (ignored on unsupported platforms) /// /// - Parameter radius: The corner radius in pixels. /// - Returns: A view with the presentation corner radius preference set. public func presentationCornerRadius(_ radius: Double) -> some View { preference(key: \.presentationCornerRadius, value: radius) } - + + /// Sets the visibility of a sheet's drag indicator. + /// + /// This modifier only affects the sheet presentation itself when applied to the + /// top-level view within a sheet. + /// + /// supported platforms: iOS 15+ (ignored on unsupported platforms) + /// + /// - Parameter visibiliy: visible or hidden + /// - Returns: A view with the presentation corner radius preference set. public func presentationDragIndicatorVisibility( _ visibility: PresentationDragIndicatorVisibility ) -> some View { preference(key: \.presentationDragIndicatorVisibility, value: visibility) } - + + /// Sets the background of a sheet. + /// + /// This modifier only affects the sheet presentation itself when applied to the + /// top-level view within a sheet. + /// + /// - Parameter color: the background color + /// - Returns: A view with the presentation corner radius preference set. public func presentationBackground(_ color: Color) -> some View { preference(key: \.presentationBackground, value: color) } - + + /// Sets wether the user should be able to dismiss the sheet themself. + /// + /// This modifier only affects the sheet presentation itself when applied to the + /// top-level view within a sheet. + /// + /// - Parameter isDisabled: is it disabled + /// - Returns: A view with the presentation corner radius preference set. public func interactiveDismissDisabled(_ isDisabled: Bool = true) -> some View { preference(key: \.interactiveDismissDisabled, value: isDisabled) } diff --git a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift index 8e2e0bd710..f0e94b8d3e 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift @@ -1,5 +1,5 @@ extension View { - /// presents a conditional modal overlay + /// Presents a conditional modal overlay /// onDismiss optional handler gets executed before /// dismissing the sheet public func sheet( From 9d6542e94bbaefea1c4d04e1ad589503b868134c Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Thu, 9 Oct 2025 10:52:50 +0200 Subject: [PATCH 13/27] Should fix UIKitBackend compile issue with visionOS and AppBackend Conformance of WinUIBackend and Gtk3Backend --- Sources/Gtk3Backend/Gtk3Backend.swift | 7 ++ Sources/GtkBackend/GtkBackend.swift | 59 ++++++++-------- Sources/SwiftCrossUI/Backend/AppBackend.swift | 4 +- .../Modifiers/PresentationModifiers.swift | 6 +- Sources/UIKitBackend/UIKitBackend+Sheet.swift | 70 ++++++++++--------- Sources/WinUIBackend/WinUIBackend.swift | 9 ++- 6 files changed, 89 insertions(+), 66 deletions(-) diff --git a/Sources/Gtk3Backend/Gtk3Backend.swift b/Sources/Gtk3Backend/Gtk3Backend.swift index ab504ef045..30c98409d7 100644 --- a/Sources/Gtk3Backend/Gtk3Backend.swift +++ b/Sources/Gtk3Backend/Gtk3Backend.swift @@ -22,6 +22,7 @@ public final class Gtk3Backend: AppBackend { public typealias Widget = Gtk3.Widget public typealias Menu = Gtk3.Menu public typealias Alert = Gtk3.MessageDialog + public typealias Sheet = Gtk3.Window public final class Path { var path: SwiftCrossUI.Path? @@ -1516,3 +1517,9 @@ struct Gtk3Error: LocalizedError { "gerror: code=\(code), domain=\(domain), message=\(message)" } } + +extension Gtk3.Window: SheetImplementation { + public var sheetSize: SIMD2 { + SIMD2(x: size.width, y: size.height) + } +} diff --git a/Sources/GtkBackend/GtkBackend.swift b/Sources/GtkBackend/GtkBackend.swift index 30f4ace11f..f3818d620e 100644 --- a/Sources/GtkBackend/GtkBackend.swift +++ b/Sources/GtkBackend/GtkBackend.swift @@ -74,34 +74,35 @@ public final class GtkBackend: AppBackend { let backend = Unmanaged.fromOpaque(userData).takeUnretainedValue() let key = OpaquePointer(instance) guard let ctx = backend.sheetContexts[key] else { return 1 } - + if ctx.interactiveDismissDisabled { return 1 } - + if ctx.isProgrammaticDismiss { ctx.isProgrammaticDismiss = false return 1 } - + backend.runInMainThread { ctx.onDismiss() } return 1 } - + // C-convention thunk for key-pressed - private let escapeKeyPressedThunk: @convention(c) ( - UnsafeMutableRawPointer?, guint, guint, GdkModifierType, gpointer? - ) -> gboolean = { controller, keyval, keycode, state, userData in - // TRUE (1) = consume event - if keyval == GDK_KEY_Escape { - guard let userData else { return 1 } - let box = Unmanaged Void>>.fromOpaque(userData).takeUnretainedValue() - box.value() - return 1 + private let escapeKeyPressedThunk: + @convention(c) ( + UnsafeMutableRawPointer?, guint, guint, GdkModifierType, gpointer? + ) -> gboolean = { controller, keyval, keycode, state, userData in + // TRUE (1) = consume event + if keyval == GDK_KEY_Escape { + guard let userData else { return 1 } + let box = Unmanaged Void>>.fromOpaque(userData).takeUnretainedValue() + box.value() + return 1 + } + return 0 } - return 0 - } - + // A separate initializer to satisfy ``AppBackend``'s requirements. public convenience init() { self.init(appIdentifier: nil) @@ -1637,7 +1638,7 @@ public final class GtkBackend: AppBackend { let ctx = getOrCreateSheetContext(for: sheet) ctx.onDismiss = onDismiss - + sheet.css.set(property: .cornerRadius(defaultSheetCornerRadius)) if connectedCloseHandlers.insert(key).inserted { @@ -1650,19 +1651,21 @@ public final class GtkBackend: AppBackend { nil, GConnectFlags(0) ) - + let escapeHandler = gtk_event_controller_key_new() gtk_event_controller_set_propagation_phase(escapeHandler, GTK_PHASE_BUBBLE) - g_signal_connect_data ( + g_signal_connect_data( UnsafeMutableRawPointer(escapeHandler), "key-pressed", unsafeBitCast(escapeKeyPressedThunk, to: GCallback.self), - Unmanaged.passRetained(ValueBox(value: { - if ctx.interactiveDismissDisabled { return } - self.runInMainThread { - ctx.onDismiss() - } - })).toOpaque(), + Unmanaged.passRetained( + ValueBox(value: { + if ctx.interactiveDismissDisabled { return } + self.runInMainThread { + ctx.onDismiss() + } + }) + ).toOpaque(), { data, _ in if let data { Unmanaged Void>>.fromOpaque(data).release() @@ -1697,15 +1700,15 @@ public final class GtkBackend: AppBackend { public func setInteractiveDismissDisabled(for sheet: Gtk.Window, to disabled: Bool) { let ctx = getOrCreateSheetContext(for: sheet) - + ctx.interactiveDismissDisabled = disabled } - + public func setPresentationCornerRadius(of sheet: Gtk.Window, to radius: Double) { let radius = Int(radius) sheet.css.set(property: .cornerRadius(radius)) } - + private func getOrCreateSheetContext(for sheet: Gtk.Window) -> SheetContext { let key: OpaquePointer = OpaquePointer(sheet.widgetPointer) if let ctx = sheetContexts[key] { diff --git a/Sources/SwiftCrossUI/Backend/AppBackend.swift b/Sources/SwiftCrossUI/Backend/AppBackend.swift index 919f341562..3cddf71532 100644 --- a/Sources/SwiftCrossUI/Backend/AppBackend.swift +++ b/Sources/SwiftCrossUI/Backend/AppBackend.swift @@ -1298,11 +1298,11 @@ extension AppBackend { ignored() } - func setPresentationBackground(of sheet: Sheet, to color: Color) { + public func setPresentationBackground(of sheet: Sheet, to color: Color) { todo() } - func setInteractiveDismissDisabled(for sheet: Sheet, to disabled: Bool) { + public func setInteractiveDismissDisabled(for sheet: Sheet, to disabled: Bool) { todo() } } diff --git a/Sources/SwiftCrossUI/Views/Modifiers/PresentationModifiers.swift b/Sources/SwiftCrossUI/Views/Modifiers/PresentationModifiers.swift index f2ffb0b99a..4967dda994 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/PresentationModifiers.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/PresentationModifiers.swift @@ -27,7 +27,7 @@ extension View { public func presentationCornerRadius(_ radius: Double) -> some View { preference(key: \.presentationCornerRadius, value: radius) } - + /// Sets the visibility of a sheet's drag indicator. /// /// This modifier only affects the sheet presentation itself when applied to the @@ -42,7 +42,7 @@ extension View { ) -> some View { preference(key: \.presentationDragIndicatorVisibility, value: visibility) } - + /// Sets the background of a sheet. /// /// This modifier only affects the sheet presentation itself when applied to the @@ -53,7 +53,7 @@ extension View { public func presentationBackground(_ color: Color) -> some View { preference(key: \.presentationBackground, value: color) } - + /// Sets wether the user should be able to dismiss the sheet themself. /// /// This modifier only affects the sheet presentation itself when applied to the diff --git a/Sources/UIKitBackend/UIKitBackend+Sheet.swift b/Sources/UIKitBackend/UIKitBackend+Sheet.swift index a0c265e5b9..ab57ab0b8e 100644 --- a/Sources/UIKitBackend/UIKitBackend+Sheet.swift +++ b/Sources/UIKitBackend/UIKitBackend+Sheet.swift @@ -39,34 +39,36 @@ extension UIKitBackend { public func setPresentationDetents(of sheet: CustomSheet, to detents: [PresentationDetent]) { if #available(iOS 15.0, *) { - if let sheetPresentation = sheet.sheetPresentationController { - sheetPresentation.detents = detents.map { - switch $0 { - case .medium: return .medium() - case .large: return .large() - case .fraction(let fraction): - if #available(iOS 16.0, *) { - return .custom( - identifier: .init("Fraction:\(fraction)"), - resolver: { context in - context.maximumDetentValue * fraction - }) - } else { - return .medium() - } - case .height(let height): - if #available(iOS 16.0, *) { - return .custom( - identifier: .init("Height:\(height)"), - resolver: { context in - height - }) - } else { - return .medium() - } + #if !os(visionOS) + if let sheetPresentation = sheet.sheetPresentationController { + sheetPresentation.detents = detents.map { + switch $0 { + case .medium: return .medium() + case .large: return .large() + case .fraction(let fraction): + if #available(iOS 16.0, *) { + return .custom( + identifier: .init("Fraction:\(fraction)"), + resolver: { context in + context.maximumDetentValue * fraction + }) + } else { + return .medium() + } + case .height(let height): + if #available(iOS 16.0, *) { + return .custom( + identifier: .init("Height:\(height)"), + resolver: { context in + height + }) + } else { + return .medium() + } + } } } - } + #endif } else { #if DEBUG print( @@ -78,9 +80,11 @@ extension UIKitBackend { public func setPresentationCornerRadius(of sheet: CustomSheet, to radius: Double) { if #available(iOS 15.0, *) { - if let sheetController = sheet.sheetPresentationController { - sheetController.preferredCornerRadius = radius - } + #if !os(visionOS) + if let sheetController = sheet.sheetPresentationController { + sheetController.preferredCornerRadius = radius + } + #endif } else { #if DEBUG print( @@ -94,9 +98,11 @@ extension UIKitBackend { of sheet: Sheet, to visibility: PresentationDragIndicatorVisibility ) { if #available(iOS 15.0, *) { - if let sheetController = sheet.sheetPresentationController { - sheetController.prefersGrabberVisible = visibility == .visible ? true : false - } + #if !os(visionOS) + if let sheetController = sheet.sheetPresentationController { + sheetController.prefersGrabberVisible = visibility == .visible ? true : false + } + #endif } else { #if DEBUG print( diff --git a/Sources/WinUIBackend/WinUIBackend.swift b/Sources/WinUIBackend/WinUIBackend.swift index a54a1a8625..d65f00934b 100644 --- a/Sources/WinUIBackend/WinUIBackend.swift +++ b/Sources/WinUIBackend/WinUIBackend.swift @@ -34,6 +34,7 @@ public final class WinUIBackend: AppBackend { public typealias Menu = Void public typealias Alert = WinUI.ContentDialog public typealias Path = GeometryGroupHolder + public typealias Sheet = CustomWindow //only for be protocol conform. doesn't currently support it public let defaultTableRowContentHeight = 20 public let defaultTableCellVerticalPadding = 4 @@ -1869,7 +1870,7 @@ class SwiftIInitializeWithWindow: WindowsFoundation.IUnknown { } } -public class CustomWindow: WinUI.Window { +public class CustomWindow: WinUI.Window, SheetImplementation { /// Hardcoded menu bar height from MenuBar_themeresources.xaml in the /// microsoft-ui-xaml repository. static let menuBarHeight = 0 @@ -1879,6 +1880,12 @@ public class CustomWindow: WinUI.Window { var grid: WinUI.Grid var cachedAppWindow: WinAppSDK.AppWindow! + //only for AppBackend conformance, no support yet + var sheetSize: SIMD2 { + let size = self.cachedAppWindow.size + return SIMD2(x: size.width, y: size.height) + } + var scaleFactor: Double { // I'm leaving this code here for future travellers. Be warned that this always // seems to return 100% even if the scale factor is set to 125% in settings. From 602ab542e5a197750f1a5cb593a99c0b97a476b1 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Thu, 9 Oct 2025 11:02:00 +0200 Subject: [PATCH 14/27] maybe ease gh actions gtk compile fix? --- Sources/GtkBackend/GtkBackend.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GtkBackend/GtkBackend.swift b/Sources/GtkBackend/GtkBackend.swift index f3818d620e..f89d9a7b28 100644 --- a/Sources/GtkBackend/GtkBackend.swift +++ b/Sources/GtkBackend/GtkBackend.swift @@ -1671,7 +1671,7 @@ public final class GtkBackend: AppBackend { Unmanaged Void>>.fromOpaque(data).release() } }, - G_CONNECT_DEFAULT + .init(0) ) gtk_widget_add_controller(sheet.widgetPointer, escapeHandler) } From af6da885d00cd1ede5acc4fe46876ba068a94c51 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Thu, 9 Oct 2025 11:29:49 +0200 Subject: [PATCH 15/27] fixed winUI AppBackend Conformance --- Sources/WinUIBackend/WinUIBackend.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/WinUIBackend/WinUIBackend.swift b/Sources/WinUIBackend/WinUIBackend.swift index d65f00934b..9e724bea69 100644 --- a/Sources/WinUIBackend/WinUIBackend.swift +++ b/Sources/WinUIBackend/WinUIBackend.swift @@ -1881,9 +1881,9 @@ public class CustomWindow: WinUI.Window, SheetImplementation { var cachedAppWindow: WinAppSDK.AppWindow! //only for AppBackend conformance, no support yet - var sheetSize: SIMD2 { + public var sheetSize: SIMD2 { let size = self.cachedAppWindow.size - return SIMD2(x: size.width, y: size.height) + return SIMD2(x: Int(size.width), y: Int(size.height)) } var scaleFactor: Double { From bb8b228a1bf46f774f28b3d2a70277b8c57573a6 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Mon, 20 Oct 2025 20:46:13 +0200 Subject: [PATCH 16/27] removed SheetImplementation Protocol, replaced with backend.sizeOf(_:) changed comment in appbackend --- Sources/AppKitBackend/AppKitBackend.swift | 15 ++++++------- Sources/GtkBackend/GtkBackend.swift | 10 ++++----- Sources/SwiftCrossUI/Backend/AppBackend.swift | 21 ++++++++++++------- .../Views/Modifiers/SheetModifier.swift | 4 ++-- Sources/UIKitBackend/UIKitBackend+Sheet.swift | 12 +++++------ Sources/WinUIBackend/WinUIBackend.swift | 8 +------ 6 files changed, 34 insertions(+), 36 deletions(-) diff --git a/Sources/AppKitBackend/AppKitBackend.swift b/Sources/AppKitBackend/AppKitBackend.swift index af49464896..f7bc3fbcb4 100644 --- a/Sources/AppKitBackend/AppKitBackend.swift +++ b/Sources/AppKitBackend/AppKitBackend.swift @@ -1716,6 +1716,13 @@ public final class AppKitBackend: AppBackend { sheet.onDismiss = onDismiss } + public func sizeOf(_ sheet: NSCustomSheet) -> SIMD2 { + guard let size = sheet.contentView?.frame.size else { + return SIMD2(x: 0, y: 0) + } + return SIMD2(x: Int(size.width), y: Int(size.height)) + } + public func showSheet(_ sheet: NSCustomSheet, window: NSCustomWindow?) { guard let window else { print("warning: Cannot show sheet without a parent window") @@ -1772,13 +1779,7 @@ public final class AppKitBackend: AppBackend { } } -public final class NSCustomSheet: NSCustomWindow, NSWindowDelegate, SheetImplementation { - public var sheetSize: SIMD2 { - guard let size = self.contentView?.frame.size else { - return SIMD2(x: 0, y: 0) - } - return SIMD2(x: Int(size.width), y: Int(size.height)) - } +public final class NSCustomSheet: NSCustomWindow, NSWindowDelegate { public var onDismiss: (() -> Void)? public var interactiveDismissDisabled: Bool = false diff --git a/Sources/GtkBackend/GtkBackend.swift b/Sources/GtkBackend/GtkBackend.swift index f89d9a7b28..977531fc79 100644 --- a/Sources/GtkBackend/GtkBackend.swift +++ b/Sources/GtkBackend/GtkBackend.swift @@ -1694,6 +1694,10 @@ public final class GtkBackend: AppBackend { connectedCloseHandlers.remove(key) } + public func sizeOf(_ sheet: Gtk.Window) -> SIMD2 { + return SIMD2(x: sheet.size.width, y: sheet.size.height) + } + public func setPresentationBackground(of sheet: Gtk.Window, to color: SwiftCrossUI.Color) { sheet.css.set(properties: [.backgroundColor(color.gtkColor)]) } @@ -1732,12 +1736,6 @@ class CustomListBox: ListBox { var cachedSelection: Int? = nil } -extension Gtk.Window: SheetImplementation { - public var sheetSize: SIMD2 { - return SIMD2(x: self.size.width, y: self.size.height) - } -} - final class ValueBox { let value: T init(value: T) { diff --git a/Sources/SwiftCrossUI/Backend/AppBackend.swift b/Sources/SwiftCrossUI/Backend/AppBackend.swift index 3cddf71532..9f2f23f56b 100644 --- a/Sources/SwiftCrossUI/Backend/AppBackend.swift +++ b/Sources/SwiftCrossUI/Backend/AppBackend.swift @@ -47,7 +47,7 @@ public protocol AppBackend: Sendable { associatedtype Menu associatedtype Alert associatedtype Path - associatedtype Sheet: SheetImplementation + associatedtype Sheet /// Creates an instance of the backend. init() @@ -604,12 +604,12 @@ public protocol AppBackend: Sendable { /// ``showAlert(_:window:responseHandler:)``. func dismissAlert(_ alert: Alert, window: Window?) - /// Creates a sheet object (without showing it yet). Sheets contain View Content. - /// They optionally execute provied code on dismiss and + /// Creates a sheet object (without showing it yet). Sheets contain view content. + /// They optionally execute provided code on dismiss and /// prevent users from interacting with the parent window until dimissed. func createSheet() -> Sheet - /// Updates the content and appearance of a sheet + /// Updates the content and appearance of a sheet. func updateSheet( _ sheet: Sheet, content: Widget, @@ -633,6 +633,9 @@ public protocol AppBackend: Sendable { /// Gets used by the SCUI sheet implementation to close a sheet. func dismissSheet(_ sheet: Sheet, window: Window?) + /// Get the dimensions of a sheet + func sizeOf(_ sheet: Sheet) -> SIMD2 + /// Sets the corner radius for a sheet presentation. /// /// This method is called when the sheet content has a `presentationCornerRadius` modifier @@ -806,10 +809,6 @@ extension AppBackend { } } -public protocol SheetImplementation { - var sheetSize: SIMD2 { get } -} - extension AppBackend { /// Used by placeholder implementations of backend methods. private func todo(_ function: String = #function) -> Never { @@ -1273,6 +1272,12 @@ extension AppBackend { todo() } + public func sizeOf( + sheet: Sheet + ) -> SIMD2 { + todo() + } + public func showSheet( _ sheet: Sheet, window: Window? diff --git a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift index f0e94b8d3e..a1c93438af 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift @@ -80,7 +80,7 @@ struct SheetModifier: TypeSafeView { let dryRunResult = children.sheetContentNode.update( with: sheetContent(), - proposedSize: sheet.sheetSize, + proposedSize: backend.sizeOf(sheet), environment: sheetEnvironment, dryRun: true ) @@ -89,7 +89,7 @@ struct SheetModifier: TypeSafeView { let _ = children.sheetContentNode.update( with: sheetContent(), - proposedSize: sheet.sheetSize, + proposedSize: backend.sizeOf(sheet), environment: sheetEnvironment, dryRun: false ) diff --git a/Sources/UIKitBackend/UIKitBackend+Sheet.swift b/Sources/UIKitBackend/UIKitBackend+Sheet.swift index ab57ab0b8e..edb9b6f0cc 100644 --- a/Sources/UIKitBackend/UIKitBackend+Sheet.swift +++ b/Sources/UIKitBackend/UIKitBackend+Sheet.swift @@ -37,6 +37,11 @@ extension UIKitBackend { } } + public func sizeOf(_ sheet: CustomSheet) -> SIMD2 { + let size = sheet.view.frame.size + return SIMD2(x: Int(size.width), y: Int(size.height)) + } + public func setPresentationDetents(of sheet: CustomSheet, to detents: [PresentationDetent]) { if #available(iOS 15.0, *) { #if !os(visionOS) @@ -121,12 +126,7 @@ extension UIKitBackend { } } -public final class CustomSheet: UIViewController, SheetImplementation { - public var sheetSize: SIMD2 { - let size = view.frame.size - return SIMD2(x: Int(size.width), y: Int(size.height)) - } - +public final class CustomSheet: UIViewController { var onDismiss: (() -> Void)? private var isDismissedProgrammatically = false diff --git a/Sources/WinUIBackend/WinUIBackend.swift b/Sources/WinUIBackend/WinUIBackend.swift index 9e724bea69..0a34a234f3 100644 --- a/Sources/WinUIBackend/WinUIBackend.swift +++ b/Sources/WinUIBackend/WinUIBackend.swift @@ -1870,7 +1870,7 @@ class SwiftIInitializeWithWindow: WindowsFoundation.IUnknown { } } -public class CustomWindow: WinUI.Window, SheetImplementation { +public class CustomWindow: WinUI.Window { /// Hardcoded menu bar height from MenuBar_themeresources.xaml in the /// microsoft-ui-xaml repository. static let menuBarHeight = 0 @@ -1880,12 +1880,6 @@ public class CustomWindow: WinUI.Window, SheetImplementation { var grid: WinUI.Grid var cachedAppWindow: WinAppSDK.AppWindow! - //only for AppBackend conformance, no support yet - public var sheetSize: SIMD2 { - let size = self.cachedAppWindow.size - return SIMD2(x: Int(size.width), y: Int(size.height)) - } - var scaleFactor: Double { // I'm leaving this code here for future travellers. Be warned that this always // seems to return 100% even if the scale factor is set to 125% in settings. From 0f53ec84a4783712ed9e6c79a7a1d9e23dd20688 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Mon, 20 Oct 2025 21:11:32 +0200 Subject: [PATCH 17/27] Adding sheet content in createSheet() instead of update (UIKit, AppKit) --- Sources/AppKitBackend/AppKitBackend.swift | 16 ++++++---------- Sources/SwiftCrossUI/Backend/AppBackend.swift | 6 ++---- .../Views/Modifiers/SheetModifier.swift | 5 +++-- Sources/UIKitBackend/UIKitBackend+Sheet.swift | 8 +++----- Sources/WinUIBackend/WinUIBackend.swift | 2 +- 5 files changed, 15 insertions(+), 22 deletions(-) diff --git a/Sources/AppKitBackend/AppKitBackend.swift b/Sources/AppKitBackend/AppKitBackend.swift index f7bc3fbcb4..12165a0dae 100644 --- a/Sources/AppKitBackend/AppKitBackend.swift +++ b/Sources/AppKitBackend/AppKitBackend.swift @@ -1687,32 +1687,28 @@ public final class AppKitBackend: AppBackend { webView.load(request) } - public func createSheet() -> NSCustomSheet { + public func createSheet(content: NSView) -> NSCustomSheet { // Initialize with a default contentRect, similar to window creation (lines 58-68) let sheet = NSCustomSheet( contentRect: NSRect( x: 0, y: 0, width: 400, // Default width - height: 300 // Default height + height: 400 // Default height ), styleMask: [.titled, .closable], backing: .buffered, defer: true ) + sheet.contentView = content + return sheet } public func updateSheet( - _ sheet: NSCustomSheet, content: NSView, onDismiss: @escaping () -> Void + _ sheet: NSCustomSheet, + onDismiss: @escaping () -> Void ) { - let contentSize = naturalSize(of: content) - - let width = max(contentSize.x, 10) - let height = max(contentSize.y, 10) - sheet.setContentSize(NSSize(width: width, height: height)) - - sheet.contentView = content sheet.onDismiss = onDismiss } diff --git a/Sources/SwiftCrossUI/Backend/AppBackend.swift b/Sources/SwiftCrossUI/Backend/AppBackend.swift index 9f2f23f56b..7b1620a690 100644 --- a/Sources/SwiftCrossUI/Backend/AppBackend.swift +++ b/Sources/SwiftCrossUI/Backend/AppBackend.swift @@ -607,12 +607,11 @@ public protocol AppBackend: Sendable { /// Creates a sheet object (without showing it yet). Sheets contain view content. /// They optionally execute provided code on dismiss and /// prevent users from interacting with the parent window until dimissed. - func createSheet() -> Sheet + func createSheet(content: Widget) -> Sheet /// Updates the content and appearance of a sheet. func updateSheet( _ sheet: Sheet, - content: Widget, onDismiss: @escaping () -> Void ) @@ -1260,13 +1259,12 @@ extension AppBackend { todo() } - public func createSheet() -> Sheet { + public func createSheet(content: Widget) -> Sheet { todo() } public func updateSheet( _ sheet: Sheet, - content: Widget, onDismiss: @escaping () -> Void ) { todo() diff --git a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift index a1c93438af..f60abc9a14 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift @@ -71,7 +71,9 @@ struct SheetModifier: TypeSafeView { ) if isPresented.wrappedValue && children.sheet == nil { - let sheet = backend.createSheet() + let sheet = backend.createSheet( + content: children.sheetContentNode.widget.into() + ) let dismissAction = DismissAction(action: { [isPresented] in isPresented.wrappedValue = false @@ -96,7 +98,6 @@ struct SheetModifier: TypeSafeView { backend.updateSheet( sheet, - content: children.sheetContentNode.widget.into(), onDismiss: handleDismiss ) diff --git a/Sources/UIKitBackend/UIKitBackend+Sheet.swift b/Sources/UIKitBackend/UIKitBackend+Sheet.swift index edb9b6f0cc..06375cb16c 100644 --- a/Sources/UIKitBackend/UIKitBackend+Sheet.swift +++ b/Sources/UIKitBackend/UIKitBackend+Sheet.swift @@ -4,16 +4,14 @@ import UIKit extension UIKitBackend { public typealias Sheet = CustomSheet - public func createSheet() -> CustomSheet { + public func createSheet(content: Widget) -> CustomSheet { let sheet = CustomSheet() sheet.modalPresentationStyle = .formSheet - + sheet.view = content.view return sheet } - public func updateSheet(_ sheet: CustomSheet, content: Widget, onDismiss: @escaping () -> Void) - { - sheet.view = content.view + public func updateSheet(_ sheet: CustomSheet, onDismiss: @escaping () -> Void) { sheet.onDismiss = onDismiss } diff --git a/Sources/WinUIBackend/WinUIBackend.swift b/Sources/WinUIBackend/WinUIBackend.swift index 0a34a234f3..cf462ec95a 100644 --- a/Sources/WinUIBackend/WinUIBackend.swift +++ b/Sources/WinUIBackend/WinUIBackend.swift @@ -34,7 +34,7 @@ public final class WinUIBackend: AppBackend { public typealias Menu = Void public typealias Alert = WinUI.ContentDialog public typealias Path = GeometryGroupHolder - public typealias Sheet = CustomWindow //only for be protocol conform. doesn't currently support it + public typealias Sheet = CustomWindow // Only for protocol conformance. WinUI doesn't currently support it. public let defaultTableRowContentHeight = 20 public let defaultTableCellVerticalPadding = 4 From bf91e02383961f359036e757aab1a77814ae5505 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Mon, 20 Oct 2025 21:17:39 +0200 Subject: [PATCH 18/27] adding sheet content on create (Gtk) --- Sources/GtkBackend/GtkBackend.swift | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Sources/GtkBackend/GtkBackend.swift b/Sources/GtkBackend/GtkBackend.swift index 977531fc79..1cd187d3aa 100644 --- a/Sources/GtkBackend/GtkBackend.swift +++ b/Sources/GtkBackend/GtkBackend.swift @@ -1624,13 +1624,14 @@ public final class GtkBackend: AppBackend { return properties } - public func createSheet() -> Gtk.Window { - return Gtk.Window() - } - - public func updateSheet(_ sheet: Gtk.Window, content: Widget, onDismiss: @escaping () -> Void) { + public func createSheet(content: Widget) -> Gtk.Window { + let sheet = Gtk.Window() sheet.setChild(content) + + return sheet + } + public func updateSheet(_ sheet: Gtk.Window, onDismiss: @escaping () -> Void) { let key: OpaquePointer = OpaquePointer(sheet.widgetPointer) //add a slight border to not be just a flat corner From 9bba11e2efd49d52b970f4c8ba3c0a2e4ba653b8 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Mon, 20 Oct 2025 21:33:53 +0200 Subject: [PATCH 19/27] comment improvements --- .../WindowingExample/WindowingApp.swift | 2 +- Sources/AppKitBackend/AppKitBackend.swift | 2 +- Sources/GtkBackend/GtkBackend.swift | 2 +- Sources/SwiftCrossUI/Backend/AppBackend.swift | 31 ++++++++++--------- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/Examples/Sources/WindowingExample/WindowingApp.swift b/Examples/Sources/WindowingExample/WindowingApp.swift index d0a79afc83..4ec5d7a38c 100644 --- a/Examples/Sources/WindowingExample/WindowingApp.swift +++ b/Examples/Sources/WindowingExample/WindowingApp.swift @@ -64,7 +64,7 @@ struct AlertDemo: View { } } -// kind of a stress test for the dismiss action +// A demo displaying SwiftCrossUI's `View.sheet` modifier. struct SheetDemo: View { @State var isPresented = false @State var isShortTermSheetPresented = false diff --git a/Sources/AppKitBackend/AppKitBackend.swift b/Sources/AppKitBackend/AppKitBackend.swift index 12165a0dae..2f31e08a33 100644 --- a/Sources/AppKitBackend/AppKitBackend.swift +++ b/Sources/AppKitBackend/AppKitBackend.swift @@ -1688,7 +1688,7 @@ public final class AppKitBackend: AppBackend { } public func createSheet(content: NSView) -> NSCustomSheet { - // Initialize with a default contentRect, similar to window creation (lines 58-68) + // Initialize with a default contentRect, similar to `createWindow` let sheet = NSCustomSheet( contentRect: NSRect( x: 0, diff --git a/Sources/GtkBackend/GtkBackend.swift b/Sources/GtkBackend/GtkBackend.swift index 1cd187d3aa..62c411f3e1 100644 --- a/Sources/GtkBackend/GtkBackend.swift +++ b/Sources/GtkBackend/GtkBackend.swift @@ -1627,7 +1627,7 @@ public final class GtkBackend: AppBackend { public func createSheet(content: Widget) -> Gtk.Window { let sheet = Gtk.Window() sheet.setChild(content) - + return sheet } diff --git a/Sources/SwiftCrossUI/Backend/AppBackend.swift b/Sources/SwiftCrossUI/Backend/AppBackend.swift index 7b1620a690..e7de9b5f97 100644 --- a/Sources/SwiftCrossUI/Backend/AppBackend.swift +++ b/Sources/SwiftCrossUI/Backend/AppBackend.swift @@ -617,7 +617,8 @@ public protocol AppBackend: Sendable { /// Shows a sheet as a modal on top of or within the given window. /// Users should be unable to interact with the parent window until the - /// sheet gets dismissed. The sheet will be closed once onDismiss gets called + /// sheet gets dismissed. + /// `onDismiss` only gets called once the sheet has been closed. /// /// Must only get called once for any given sheet. /// @@ -629,7 +630,7 @@ public protocol AppBackend: Sendable { ) /// Dismisses a sheet programmatically. - /// Gets used by the SCUI sheet implementation to close a sheet. + /// Gets used by the ``View/sheet`` modifier to close a sheet. func dismissSheet(_ sheet: Sheet, window: Window?) /// Get the dimensions of a sheet @@ -637,8 +638,8 @@ public protocol AppBackend: Sendable { /// Sets the corner radius for a sheet presentation. /// - /// This method is called when the sheet content has a `presentationCornerRadius` modifier - /// applied at its top level. The corner radius affects the sheet's presentation container, + /// This method is called when the sheet content has a `presentationCornerRadius` + /// preference key set. The corner radius affects the sheet's presentation container, /// not the content itself. /// /// - Parameters: @@ -648,8 +649,8 @@ public protocol AppBackend: Sendable { /// Sets the available detents (heights) for a sheet presentation. /// - /// This method is called when the sheet content has a `presentationDetents` modifier - /// applied at its top level. Detents allow users to resize the sheet to predefined heights. + /// This method is called when the sheet content has a `presentationDetents` + /// preference key set. Detents allow users to resize the sheet to predefined heights. /// /// - Parameters: /// - sheet: The sheet to apply the detents to. @@ -659,10 +660,10 @@ public protocol AppBackend: Sendable { /// Sets the visibility for a sheet presentation. /// /// This method is called when the sheet content has a `presentationDragIndicatorVisibility` - /// modifier applied at its top level. + /// preference key set. /// /// - Parameters: - /// - sheet: The sheet to apply the detents to. + /// - sheet: The sheet to apply the drag indicator visibility to. /// - visibility: visibility of the drag indicator (visible or hidden) func setPresentationDragIndicatorVisibility( of sheet: Sheet, @@ -672,23 +673,23 @@ public protocol AppBackend: Sendable { /// Sets the background color for a sheet presentation. /// /// This method is called when the sheet content has a `presentationBackground` - /// modifier applied at its top level. + /// preference key set. /// /// - Parameters: - /// - sheet: The sheet to apply the detents to. - /// - color: rgba background color + /// - sheet: The sheet to apply the background to. + /// - color: Background color for the sheet func setPresentationBackground(of sheet: Sheet, to color: Color) - /// Sets the interactive dismissablility of a sheet. + /// Sets the interactive dismissibility of a sheet. /// when disabled the sheet can only be closed programmatically, /// not through users swiping, escape keys or similar. /// /// This method is called when the sheet content has a `interactiveDismissDisabled` - /// modifier applied at its top level. + /// preference key set. /// /// - Parameters: - /// - sheet: The sheet to apply the detents to. - /// - disabled: wether its disabled + /// - sheet: The sheet to apply the interactive dismissability to. + /// - disabled: Whether interactive dismissing is disabled. func setInteractiveDismissDisabled(for sheet: Sheet, to disabled: Bool) /// Presents an 'Open file' dialog to the user for selecting files or From b3cfc044073aec7cdeebe6ff48003ee079db9583 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Mon, 20 Oct 2025 21:46:49 +0200 Subject: [PATCH 20/27] maybe part of letting scui dictate size? --- Sources/SwiftCrossUI/Backend/AppBackend.swift | 2 ++ Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftCrossUI/Backend/AppBackend.swift b/Sources/SwiftCrossUI/Backend/AppBackend.swift index e7de9b5f97..f662c268de 100644 --- a/Sources/SwiftCrossUI/Backend/AppBackend.swift +++ b/Sources/SwiftCrossUI/Backend/AppBackend.swift @@ -612,6 +612,7 @@ public protocol AppBackend: Sendable { /// Updates the content and appearance of a sheet. func updateSheet( _ sheet: Sheet, + proposedSize: SIMD2, onDismiss: @escaping () -> Void ) @@ -1266,6 +1267,7 @@ extension AppBackend { public func updateSheet( _ sheet: Sheet, + proposedSize: SIMD2, onDismiss: @escaping () -> Void ) { todo() diff --git a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift index f60abc9a14..8f4ffab8f0 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift @@ -91,13 +91,14 @@ struct SheetModifier: TypeSafeView { let _ = children.sheetContentNode.update( with: sheetContent(), - proposedSize: backend.sizeOf(sheet), + proposedSize: dryRunResult.size.idealSize, environment: sheetEnvironment, dryRun: false ) backend.updateSheet( sheet, + proposedSize: dryRunResult.size.idealSize, onDismiss: handleDismiss ) From 7d0a0306a95c333a8206e5f863508fb959709623 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Mon, 20 Oct 2025 22:01:25 +0200 Subject: [PATCH 21/27] changes dismissSheet now expects non optional Window rolled back proposed size try --- Sources/AppKitBackend/AppKitBackend.swift | 13 ++++--------- Sources/GtkBackend/GtkBackend.swift | 2 +- Sources/SwiftCrossUI/Backend/AppBackend.swift | 6 ++---- .../Views/Modifiers/SheetModifier.swift | 5 ++--- Sources/UIKitBackend/UIKitBackend+Sheet.swift | 2 +- 5 files changed, 10 insertions(+), 18 deletions(-) diff --git a/Sources/AppKitBackend/AppKitBackend.swift b/Sources/AppKitBackend/AppKitBackend.swift index 2f31e08a33..3aea6efaa6 100644 --- a/Sources/AppKitBackend/AppKitBackend.swift +++ b/Sources/AppKitBackend/AppKitBackend.swift @@ -1724,18 +1724,13 @@ public final class AppKitBackend: AppBackend { print("warning: Cannot show sheet without a parent window") return } - // critical sheets stack - // beginSheet only shows a nested - // sheet after its parent gets dismissed + // Critical sheets stack. beginSheet only shows a nested sheet + // after its parent gets dismissed. window.beginCriticalSheet(sheet) } - public func dismissSheet(_ sheet: NSCustomSheet, window: NSCustomWindow?) { - if let window { - window.endSheet(sheet) - } else { - NSApplication.shared.stopModal() - } + public func dismissSheet(_ sheet: NSCustomSheet, window: NSCustomWindow) { + window.endSheet(sheet) } public func setPresentationBackground(of sheet: NSCustomSheet, to color: Color) { diff --git a/Sources/GtkBackend/GtkBackend.swift b/Sources/GtkBackend/GtkBackend.swift index 62c411f3e1..372ae8b7fb 100644 --- a/Sources/GtkBackend/GtkBackend.swift +++ b/Sources/GtkBackend/GtkBackend.swift @@ -1685,7 +1685,7 @@ public final class GtkBackend: AppBackend { sheet.present() } - public func dismissSheet(_ sheet: Gtk.Window, window: ApplicationWindow?) { + public func dismissSheet(_ sheet: Gtk.Window, window: ApplicationWindow) { let key: OpaquePointer = OpaquePointer(sheet.widgetPointer) if let ctx = sheetContexts[key] { ctx.isProgrammaticDismiss = true diff --git a/Sources/SwiftCrossUI/Backend/AppBackend.swift b/Sources/SwiftCrossUI/Backend/AppBackend.swift index f662c268de..226f63fd3b 100644 --- a/Sources/SwiftCrossUI/Backend/AppBackend.swift +++ b/Sources/SwiftCrossUI/Backend/AppBackend.swift @@ -612,7 +612,6 @@ public protocol AppBackend: Sendable { /// Updates the content and appearance of a sheet. func updateSheet( _ sheet: Sheet, - proposedSize: SIMD2, onDismiss: @escaping () -> Void ) @@ -632,7 +631,7 @@ public protocol AppBackend: Sendable { /// Dismisses a sheet programmatically. /// Gets used by the ``View/sheet`` modifier to close a sheet. - func dismissSheet(_ sheet: Sheet, window: Window?) + func dismissSheet(_ sheet: Sheet, window: Window) /// Get the dimensions of a sheet func sizeOf(_ sheet: Sheet) -> SIMD2 @@ -1267,7 +1266,6 @@ extension AppBackend { public func updateSheet( _ sheet: Sheet, - proposedSize: SIMD2, onDismiss: @escaping () -> Void ) { todo() @@ -1286,7 +1284,7 @@ extension AppBackend { todo() } - public func dismissSheet(_ sheet: Sheet, window: Window?) { + public func dismissSheet(_ sheet: Sheet, window: Window) { todo() } diff --git a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift index 8f4ffab8f0..99edb6aea4 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift @@ -91,14 +91,13 @@ struct SheetModifier: TypeSafeView { let _ = children.sheetContentNode.update( with: sheetContent(), - proposedSize: dryRunResult.size.idealSize, + proposedSize: backend.sizeOf(sheet), environment: sheetEnvironment, dryRun: false ) backend.updateSheet( sheet, - proposedSize: dryRunResult.size.idealSize, onDismiss: handleDismiss ) @@ -134,7 +133,7 @@ struct SheetModifier: TypeSafeView { } else if !isPresented.wrappedValue && children.sheet != nil { backend.dismissSheet( children.sheet as! Backend.Sheet, - window: .some(environment.window! as! Backend.Window) + window: environment.window! as! Backend.Window ) children.sheet = nil } diff --git a/Sources/UIKitBackend/UIKitBackend+Sheet.swift b/Sources/UIKitBackend/UIKitBackend+Sheet.swift index 06375cb16c..0688018584 100644 --- a/Sources/UIKitBackend/UIKitBackend+Sheet.swift +++ b/Sources/UIKitBackend/UIKitBackend+Sheet.swift @@ -23,7 +23,7 @@ extension UIKitBackend { topController?.present(sheet, animated: true) } - public func dismissSheet(_ sheet: CustomSheet, window: UIWindow?) { + public func dismissSheet(_ sheet: CustomSheet, window: UIWindow) { // If this sheet has a presented view controller (nested sheet), dismiss it first if let presentedVC = sheet.presentedViewController { presentedVC.dismiss(animated: false) { [weak sheet] in From d5bf60bd3deb505e2787fe7e134a6723a0d926f5 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Mon, 20 Oct 2025 23:28:56 +0200 Subject: [PATCH 22/27] moved signals to Gtk/Widgets/Window --- Sources/Gtk/Widgets/Window.swift | 70 +++++++++++++++++++++++++++++ Sources/GtkBackend/GtkBackend.swift | 70 +++++++++++------------------ 2 files changed, 95 insertions(+), 45 deletions(-) diff --git a/Sources/Gtk/Widgets/Window.swift b/Sources/Gtk/Widgets/Window.swift index 059cb65743..04c70f12fb 100644 --- a/Sources/Gtk/Widgets/Window.swift +++ b/Sources/Gtk/Widgets/Window.swift @@ -82,5 +82,75 @@ open class Window: Widget { public func present() { gtk_window_present(castedPointer()) + + addSignal(name: "close-request") { [weak self] () in + guard let self = self else { return } + self.onCloseRequest?(self) + } + } + + public func setEscapeKeyPressedHandler(to handler: (() -> Void)?) { + if let data = escapeKeyHandlerData { + Unmanaged Void>>.fromOpaque(data).release() + escapeKeyHandlerData = nil + } + + if let oldController = escapeKeyEventController { + gtk_widget_remove_controller(widgetPointer, oldController) + escapeKeyEventController = nil + } + + escapeKeyPressed = handler + + guard handler != nil else { return } + + let keyEventController = gtk_event_controller_key_new() + gtk_event_controller_set_propagation_phase(keyEventController, GTK_PHASE_BUBBLE) + + let thunk: @convention(c) ( + UnsafeMutableRawPointer?, guint, guint, GdkModifierType, gpointer? + ) -> gboolean = { _, keyval, _, _, userData in + if keyval == GDK_KEY_Escape { + guard let userData else { return 1 } + let box = Unmanaged Void>>.fromOpaque(userData).takeUnretainedValue() + box.value() + return 1 + } + return 0 + } + + let boxedHandler = Unmanaged.passRetained( + ValueBox(value: handler!) + ).toOpaque() + + g_signal_connect_data( + UnsafeMutableRawPointer(keyEventController), + "key-pressed", + unsafeBitCast(thunk, to: GCallback.self), + boxedHandler, + { data, _ in + if let data { + Unmanaged Void>>.fromOpaque(data).release() + } + }, + .init(0) + ) + + gtk_widget_add_controller(widgetPointer, keyEventController) + escapeKeyEventController = keyEventController + escapeKeyHandlerData = boxedHandler + } + + private var escapeKeyEventController: OpaquePointer? + private var escapeKeyHandlerData: UnsafeMutableRawPointer? + + public var onCloseRequest: ((Window) -> Int32)? + public var escapeKeyPressed: (() -> Void)? +} + +final class ValueBox { + let value: T + init(value: T) { + self.value = value } } diff --git a/Sources/GtkBackend/GtkBackend.swift b/Sources/GtkBackend/GtkBackend.swift index 372ae8b7fb..3bad463c95 100644 --- a/Sources/GtkBackend/GtkBackend.swift +++ b/Sources/GtkBackend/GtkBackend.swift @@ -64,31 +64,7 @@ public final class GtkBackend: AppBackend { private var sheetContexts: [OpaquePointer: SheetContext] = [:] private var connectedCloseHandlers: Set = [] - // C thunk for GtkWindow::close-request - private static let closeRequestThunk: - @convention(c) ( - UnsafeMutableRawPointer?, UnsafeMutableRawPointer? - ) -> Int32 = { instance, userData in - // TRUE (1) = consume event (prevent native close) - guard let instance, let userData else { return 1 } - let backend = Unmanaged.fromOpaque(userData).takeUnretainedValue() - let key = OpaquePointer(instance) - guard let ctx = backend.sheetContexts[key] else { return 1 } - - if ctx.interactiveDismissDisabled { return 1 } - - if ctx.isProgrammaticDismiss { - ctx.isProgrammaticDismiss = false - return 1 - } - - backend.runInMainThread { - ctx.onDismiss() - } - return 1 - } - - // C-convention thunk for key-pressed + /* // C-convention thunk for key-pressed private let escapeKeyPressedThunk: @convention(c) ( UnsafeMutableRawPointer?, guint, guint, GdkModifierType, gpointer? @@ -102,7 +78,7 @@ public final class GtkBackend: AppBackend { } return 0 } - +*/ // A separate initializer to satisfy ``AppBackend``'s requirements. public convenience init() { self.init(appIdentifier: nil) @@ -1643,17 +1619,21 @@ public final class GtkBackend: AppBackend { sheet.css.set(property: .cornerRadius(defaultSheetCornerRadius)) if connectedCloseHandlers.insert(key).inserted { - let handler: GCallback = unsafeBitCast(Self.closeRequestThunk, to: GCallback.self) - g_signal_connect_data( - UnsafeMutableRawPointer(sheet.gobjectPointer), - "close-request", - handler, - Unmanaged.passUnretained(self).toOpaque(), - nil, - GConnectFlags(0) - ) + sheet.onCloseRequest = {[weak self] _ in + if ctx.interactiveDismissDisabled { return 1 } + + if ctx.isProgrammaticDismiss { + ctx.isProgrammaticDismiss = false + return 1 + } + + self?.runInMainThread { + ctx.onDismiss() + } + return 1 + } - let escapeHandler = gtk_event_controller_key_new() + /* let escapeHandler = gtk_event_controller_key_new() gtk_event_controller_set_propagation_phase(escapeHandler, GTK_PHASE_BUBBLE) g_signal_connect_data( UnsafeMutableRawPointer(escapeHandler), @@ -1673,8 +1653,15 @@ public final class GtkBackend: AppBackend { } }, .init(0) - ) - gtk_widget_add_controller(sheet.widgetPointer, escapeHandler) + )*/ + sheet.setEscapeKeyPressedHandler { + print("escapeKeyPressed") + if ctx.interactiveDismissDisabled { return } + self.runInMainThread { + ctx.onDismiss() + } + } + } } @@ -1736,10 +1723,3 @@ extension UnsafeMutablePointer { class CustomListBox: ListBox { var cachedSelection: Int? = nil } - -final class ValueBox { - let value: T - init(value: T) { - self.value = value - } -} From 8c9d96fb201e45eb19fb33def7818bd200128c3d Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Mon, 20 Oct 2025 23:32:44 +0200 Subject: [PATCH 23/27] removed now unecessary old code --- Sources/GtkBackend/GtkBackend.swift | 36 ----------------------------- 1 file changed, 36 deletions(-) diff --git a/Sources/GtkBackend/GtkBackend.swift b/Sources/GtkBackend/GtkBackend.swift index 3bad463c95..2d692226f9 100644 --- a/Sources/GtkBackend/GtkBackend.swift +++ b/Sources/GtkBackend/GtkBackend.swift @@ -64,21 +64,6 @@ public final class GtkBackend: AppBackend { private var sheetContexts: [OpaquePointer: SheetContext] = [:] private var connectedCloseHandlers: Set = [] - /* // C-convention thunk for key-pressed - private let escapeKeyPressedThunk: - @convention(c) ( - UnsafeMutableRawPointer?, guint, guint, GdkModifierType, gpointer? - ) -> gboolean = { controller, keyval, keycode, state, userData in - // TRUE (1) = consume event - if keyval == GDK_KEY_Escape { - guard let userData else { return 1 } - let box = Unmanaged Void>>.fromOpaque(userData).takeUnretainedValue() - box.value() - return 1 - } - return 0 - } -*/ // A separate initializer to satisfy ``AppBackend``'s requirements. public convenience init() { self.init(appIdentifier: nil) @@ -1633,27 +1618,6 @@ public final class GtkBackend: AppBackend { return 1 } - /* let escapeHandler = gtk_event_controller_key_new() - gtk_event_controller_set_propagation_phase(escapeHandler, GTK_PHASE_BUBBLE) - g_signal_connect_data( - UnsafeMutableRawPointer(escapeHandler), - "key-pressed", - unsafeBitCast(escapeKeyPressedThunk, to: GCallback.self), - Unmanaged.passRetained( - ValueBox(value: { - if ctx.interactiveDismissDisabled { return } - self.runInMainThread { - ctx.onDismiss() - } - }) - ).toOpaque(), - { data, _ in - if let data { - Unmanaged Void>>.fromOpaque(data).release() - } - }, - .init(0) - )*/ sheet.setEscapeKeyPressedHandler { print("escapeKeyPressed") if ctx.interactiveDismissDisabled { return } From 022274b8d9ea33be22dd0d9edbef606ad8f8b7a2 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Tue, 21 Oct 2025 00:01:55 +0200 Subject: [PATCH 24/27] mostly comment changes --- Sources/GtkBackend/GtkBackend.swift | 2 +- Sources/SwiftCrossUI/Backend/AppBackend.swift | 4 +- .../Values/PresentationDetent.swift | 4 +- .../PresentationDragIndicatorVisibility.swift | 3 -- Sources/SwiftCrossUI/Values/Visibility.swift | 3 ++ .../ViewGraph/PreferenceValues.swift | 30 +++++++------ .../Modifiers/PresentationModifiers.swift | 43 ++++++++----------- .../Views/Modifiers/SheetModifier.swift | 11 ++--- Sources/UIKitBackend/UIKitBackend+Sheet.swift | 8 ++-- 9 files changed, 53 insertions(+), 55 deletions(-) delete mode 100644 Sources/SwiftCrossUI/Values/PresentationDragIndicatorVisibility.swift create mode 100644 Sources/SwiftCrossUI/Values/Visibility.swift diff --git a/Sources/GtkBackend/GtkBackend.swift b/Sources/GtkBackend/GtkBackend.swift index 2d692226f9..f825307d20 100644 --- a/Sources/GtkBackend/GtkBackend.swift +++ b/Sources/GtkBackend/GtkBackend.swift @@ -1595,7 +1595,7 @@ public final class GtkBackend: AppBackend { public func updateSheet(_ sheet: Gtk.Window, onDismiss: @escaping () -> Void) { let key: OpaquePointer = OpaquePointer(sheet.widgetPointer) - //add a slight border to not be just a flat corner + // Add a slight border to not be just a flat corner sheet.css.set(property: .border(color: SwiftCrossUI.Color.gray.gtkColor, width: 1)) let ctx = getOrCreateSheetContext(for: sheet) diff --git a/Sources/SwiftCrossUI/Backend/AppBackend.swift b/Sources/SwiftCrossUI/Backend/AppBackend.swift index 226f63fd3b..2eb27efb05 100644 --- a/Sources/SwiftCrossUI/Backend/AppBackend.swift +++ b/Sources/SwiftCrossUI/Backend/AppBackend.swift @@ -667,7 +667,7 @@ public protocol AppBackend: Sendable { /// - visibility: visibility of the drag indicator (visible or hidden) func setPresentationDragIndicatorVisibility( of sheet: Sheet, - to visibility: PresentationDragIndicatorVisibility + to visibility: Visibility ) /// Sets the background color for a sheet presentation. @@ -1297,7 +1297,7 @@ extension AppBackend { } public func setPresentationDragIndicatorVisibility( - of sheet: Sheet, to visibility: PresentationDragIndicatorVisibility + of sheet: Sheet, to visibility: Visibility ) { ignored() } diff --git a/Sources/SwiftCrossUI/Values/PresentationDetent.swift b/Sources/SwiftCrossUI/Values/PresentationDetent.swift index a8b24c5782..f25ba037a0 100644 --- a/Sources/SwiftCrossUI/Values/PresentationDetent.swift +++ b/Sources/SwiftCrossUI/Values/PresentationDetent.swift @@ -11,8 +11,8 @@ public enum PresentationDetent: Sendable, Hashable { /// - Parameter fraction: A value between 0 and 1 representing the fraction of available height. case fraction(Double) - /// A detent at a specific fixed height in pixels. - /// falling back to medium on iOS 15 + /// A detent at a specific fixed height in points. + /// Falls back to medium on iOS 15 /// - Parameter height: The height case height(Double) } diff --git a/Sources/SwiftCrossUI/Values/PresentationDragIndicatorVisibility.swift b/Sources/SwiftCrossUI/Values/PresentationDragIndicatorVisibility.swift deleted file mode 100644 index da2cd54978..0000000000 --- a/Sources/SwiftCrossUI/Values/PresentationDragIndicatorVisibility.swift +++ /dev/null @@ -1,3 +0,0 @@ -public enum PresentationDragIndicatorVisibility: Sendable { - case hidden, visible -} diff --git a/Sources/SwiftCrossUI/Values/Visibility.swift b/Sources/SwiftCrossUI/Values/Visibility.swift new file mode 100644 index 0000000000..88a5aa2c35 --- /dev/null +++ b/Sources/SwiftCrossUI/Values/Visibility.swift @@ -0,0 +1,3 @@ +public enum Visibility: Sendable { + case automatic, hidden, visible +} diff --git a/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift b/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift index df0e0f446f..ccc66856ac 100644 --- a/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift +++ b/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift @@ -12,25 +12,26 @@ public struct PreferenceValues: Sendable { public var onOpenURL: (@Sendable @MainActor (URL) -> Void)? - /// The available detents for a sheet presentation. Only applies to the top-level view in a sheet. + /// The available detents for a sheet presentation. Applies to enclosing sheets. public var presentationDetents: [PresentationDetent]? - /// The corner radius for a sheet presentation. Only applies to the top-level view in a sheet. + /// The corner radius for a sheet presentation. Applies to enclosing sheets. public var presentationCornerRadius: Double? - /// The drag indicator visibility for a sheet presentation. Only applies to the top-level view in a sheet. - public var presentationDragIndicatorVisibility: PresentationDragIndicatorVisibility? + /// The drag indicator visibility for a sheet presentation. Applies to enclosing sheets. + public var presentationDragIndicatorVisibility: Visibility? - /// The backgroundcolor of a sheet. Only applies to the top-level view in a sheet + /// The backgroundcolor of a sheet. Applies to enclosing sheets. public var presentationBackground: Color? - + + /// Controls whether the user can interactively dismiss enclosing sheets. Applies to enclosing sheets. public var interactiveDismissDisabled: Bool? public init( onOpenURL: (@Sendable @MainActor (URL) -> Void)?, presentationDetents: [PresentationDetent]? = nil, presentationCornerRadius: Double? = nil, - presentationDragIndicatorVisibility: PresentationDragIndicatorVisibility? = nil, + presentationDragIndicatorVisibility: Visibility? = nil, presentationBackground: Color? = nil, interactiveDismissDisabled: Bool? = nil ) { @@ -53,12 +54,13 @@ public struct PreferenceValues: Sendable { } } - // For presentation modifiers, take the first (top-level) value only - // This ensures only the root view's presentation modifiers apply to the sheet - presentationDetents = children.first?.presentationDetents - presentationCornerRadius = children.first?.presentationCornerRadius - presentationDragIndicatorVisibility = children.first?.presentationDragIndicatorVisibility - presentationBackground = children.first?.presentationBackground - interactiveDismissDisabled = children.first?.interactiveDismissDisabled + // For presentation modifiers, take the outer-most value (using child ordering to break ties). + presentationDetents = children.compactMap { $0.presentationDetents }.first + presentationCornerRadius = children.compactMap { $0.presentationCornerRadius }.first + presentationDragIndicatorVisibility = children.compactMap { + $0.presentationDragIndicatorVisibility + }.first + presentationBackground = children.compactMap { $0.presentationBackground }.first + interactiveDismissDisabled = children.compactMap { $0.interactiveDismissDisabled }.first } } diff --git a/Sources/SwiftCrossUI/Views/Modifiers/PresentationModifiers.swift b/Sources/SwiftCrossUI/Views/Modifiers/PresentationModifiers.swift index 4967dda994..7d577d006c 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/PresentationModifiers.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/PresentationModifiers.swift @@ -1,13 +1,11 @@ extension View { /// Sets the available detents (heights) for a sheet presentation. /// - /// This modifier only affects the sheet presentation itself when applied to the - /// top-level view within a sheet. It allows users to resize the sheet to different - /// predefined heights. + /// This modifier only affects the sheet presentation itself. + /// It allows users to resize the sheet to different predefined heights. /// - /// supported platforms: iOS (ignored on unsupported platforms) - /// ignored on: older than iOS 15 - /// fraction and height fall back to medium on iOS 15 and work as you'd expect on >=16 + /// - Supported platforms: iOS 15+ (ignored on unsupported platforms) + /// - `.fraction` and `.height` fall back to `.medium` on iOS 15 and earlier /// /// - Parameter detents: A set of detents that the sheet can be resized to. /// - Returns: A view with the presentation detents preference set. @@ -17,12 +15,12 @@ extension View { /// Sets the corner radius for a sheet presentation. /// - /// This modifier only affects the sheet presentation itself when applied to the - /// top-level view within a sheet. It does not affect the content's corner radius. + /// This modifier only affects the sheet presentation itself. + /// It does not affect the content's corner radius. /// - /// supported platforms: iOS 15+, Gtk4 (ignored on unsupported platforms) + /// - Supported platforms: iOS 15+, Gtk4 (ignored on unsupported platforms) /// - /// - Parameter radius: The corner radius in pixels. + /// - Parameter radius: The corner radius in points. /// - Returns: A view with the presentation corner radius preference set. public func presentationCornerRadius(_ radius: Double) -> some View { preference(key: \.presentationCornerRadius, value: radius) @@ -30,37 +28,34 @@ extension View { /// Sets the visibility of a sheet's drag indicator. /// - /// This modifier only affects the sheet presentation itself when applied to the - /// top-level view within a sheet. + /// This modifier only affects the sheet presentation itself. /// - /// supported platforms: iOS 15+ (ignored on unsupported platforms) + /// - Supported platforms: iOS 15+ (ignored on unsupported platforms) /// - /// - Parameter visibiliy: visible or hidden - /// - Returns: A view with the presentation corner radius preference set. + /// - Parameter visibility: visible or hidden + /// - Returns: A view with the presentationDragIndicatorVisibility preference set. public func presentationDragIndicatorVisibility( - _ visibility: PresentationDragIndicatorVisibility + _ visibility: Visibility ) -> some View { preference(key: \.presentationDragIndicatorVisibility, value: visibility) } /// Sets the background of a sheet. /// - /// This modifier only affects the sheet presentation itself when applied to the - /// top-level view within a sheet. + /// This modifier only affects the sheet presentation itself. /// /// - Parameter color: the background color - /// - Returns: A view with the presentation corner radius preference set. + /// - Returns: A view with the presentationBackground preference set. public func presentationBackground(_ color: Color) -> some View { preference(key: \.presentationBackground, value: color) } - /// Sets wether the user should be able to dismiss the sheet themself. + /// Sets whether the user should be able to dismiss the sheet themself. /// - /// This modifier only affects the sheet presentation itself when applied to the - /// top-level view within a sheet. + /// This modifier only affects the sheet presentation itself. /// - /// - Parameter isDisabled: is it disabled - /// - Returns: A view with the presentation corner radius preference set. + /// - Parameter isDisabled: Whether interactive dismissal is disabled + /// - Returns: A view with the interactiveDismissDisabled preference set. public func interactiveDismissDisabled(_ isDisabled: Bool = true) -> some View { preference(key: \.interactiveDismissDisabled, value: isDisabled) } diff --git a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift index 99edb6aea4..4ba31e5e0b 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift @@ -1,14 +1,15 @@ extension View { - /// Presents a conditional modal overlay - /// onDismiss optional handler gets executed before - /// dismissing the sheet + /// Presents a conditional modal overlay. `onDismiss` gets invoked when the sheet is dismissed. public func sheet( isPresented: Binding, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping () -> SheetContent ) -> some View { SheetModifier( - isPresented: isPresented, body: TupleView1(self), onDismiss: onDismiss, - sheetContent: content) + isPresented: isPresented, + body: TupleView1(self), + onDismiss: onDismiss, + sheetContent: content + ) } } diff --git a/Sources/UIKitBackend/UIKitBackend+Sheet.swift b/Sources/UIKitBackend/UIKitBackend+Sheet.swift index 0688018584..72733d0936 100644 --- a/Sources/UIKitBackend/UIKitBackend+Sheet.swift +++ b/Sources/UIKitBackend/UIKitBackend+Sheet.swift @@ -75,7 +75,7 @@ extension UIKitBackend { } else { #if DEBUG print( - "your current OS Version doesn't support variable sheet heights.\n Setting presentationDetents is only available from iOS 15.0" + "your current OS Version doesn't support variable sheet heights. Setting presentationDetents is only available from iOS 15.0" ) #endif } @@ -91,14 +91,14 @@ extension UIKitBackend { } else { #if DEBUG print( - "your current OS Version doesn't support variable sheet corner radii.\n Setting them is only available from iOS 15.0" + "your current OS Version doesn't support variable sheet corner radii. Setting them is only available from iOS 15.0" ) #endif } } public func setPresentationDragIndicatorVisibility( - of sheet: Sheet, to visibility: PresentationDragIndicatorVisibility + of sheet: Sheet, to visibility: Visibility ) { if #available(iOS 15.0, *) { #if !os(visionOS) @@ -109,7 +109,7 @@ extension UIKitBackend { } else { #if DEBUG print( - "Your current OS Version doesn't support setting sheet drag indicator visibility.\n Setting this is only available from iOS 15.0" + "Your current OS Version doesn't support setting sheet drag indicator visibility. Setting this is only available from iOS 15.0" ) #endif } From 17875d2878f690ff3d55a929225849e5a27f0418 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Tue, 21 Oct 2025 00:59:00 +0200 Subject: [PATCH 25/27] 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 --- .../WindowingExample/WindowingApp.swift | 9 ++++ Sources/Gtk/Widgets/Window.swift | 44 ++++++++++--------- Sources/GtkBackend/GtkBackend.swift | 4 +- .../ViewGraph/PreferenceValues.swift | 9 ++-- .../Views/Modifiers/SheetModifier.swift | 40 ++++++++++------- 5 files changed, 63 insertions(+), 43 deletions(-) diff --git a/Examples/Sources/WindowingExample/WindowingApp.swift b/Examples/Sources/WindowingExample/WindowingApp.swift index 4ec5d7a38c..a5b1a98bf9 100644 --- a/Examples/Sources/WindowingExample/WindowingApp.swift +++ b/Examples/Sources/WindowingExample/WindowingApp.swift @@ -135,6 +135,12 @@ struct SheetDemo: View { .sheet(isPresented: $showNextChild) { DoubleNestedSheetBody(dismissParent: { dismiss() }) .interactiveDismissDisabled() + .onAppear { + print("deepest nested sheet appeared") + } + .onDisappear { + print("deepest nested sheet disappeared") + } } Button("dismiss parent sheet") { dismissParent() @@ -142,6 +148,9 @@ struct SheetDemo: View { Button("dismiss") { dismiss() } + .onDisappear { + print("nested sheet disappeared") + } } } struct DoubleNestedSheetBody: View { diff --git a/Sources/Gtk/Widgets/Window.swift b/Sources/Gtk/Widgets/Window.swift index 04c70f12fb..364d3edfc9 100644 --- a/Sources/Gtk/Widgets/Window.swift +++ b/Sources/Gtk/Widgets/Window.swift @@ -82,7 +82,7 @@ open class Window: Widget { public func present() { gtk_window_present(castedPointer()) - + addSignal(name: "close-request") { [weak self] () in guard let self = self else { return } self.onCloseRequest?(self) @@ -94,35 +94,37 @@ open class Window: Widget { Unmanaged Void>>.fromOpaque(data).release() escapeKeyHandlerData = nil } - + if let oldController = escapeKeyEventController { gtk_widget_remove_controller(widgetPointer, oldController) escapeKeyEventController = nil } - + escapeKeyPressed = handler - + guard handler != nil else { return } - + let keyEventController = gtk_event_controller_key_new() gtk_event_controller_set_propagation_phase(keyEventController, GTK_PHASE_BUBBLE) - - let thunk: @convention(c) ( - UnsafeMutableRawPointer?, guint, guint, GdkModifierType, gpointer? - ) -> gboolean = { _, keyval, _, _, userData in - if keyval == GDK_KEY_Escape { - guard let userData else { return 1 } - let box = Unmanaged Void>>.fromOpaque(userData).takeUnretainedValue() - box.value() - return 1 + + let thunk: + @convention(c) ( + UnsafeMutableRawPointer?, guint, guint, GdkModifierType, gpointer? + ) -> gboolean = { _, keyval, _, _, userData in + if keyval == GDK_KEY_Escape { + guard let userData else { return 1 } + let box = Unmanaged Void>>.fromOpaque(userData) + .takeUnretainedValue() + box.value() + return 1 + } + return 0 } - return 0 - } - + let boxedHandler = Unmanaged.passRetained( ValueBox(value: handler!) ).toOpaque() - + g_signal_connect_data( UnsafeMutableRawPointer(keyEventController), "key-pressed", @@ -135,15 +137,15 @@ open class Window: Widget { }, .init(0) ) - + gtk_widget_add_controller(widgetPointer, keyEventController) escapeKeyEventController = keyEventController escapeKeyHandlerData = boxedHandler } - + private var escapeKeyEventController: OpaquePointer? private var escapeKeyHandlerData: UnsafeMutableRawPointer? - + public var onCloseRequest: ((Window) -> Int32)? public var escapeKeyPressed: (() -> Void)? } diff --git a/Sources/GtkBackend/GtkBackend.swift b/Sources/GtkBackend/GtkBackend.swift index f825307d20..27c303d3d6 100644 --- a/Sources/GtkBackend/GtkBackend.swift +++ b/Sources/GtkBackend/GtkBackend.swift @@ -1604,7 +1604,7 @@ public final class GtkBackend: AppBackend { sheet.css.set(property: .cornerRadius(defaultSheetCornerRadius)) if connectedCloseHandlers.insert(key).inserted { - sheet.onCloseRequest = {[weak self] _ in + sheet.onCloseRequest = { [weak self] _ in if ctx.interactiveDismissDisabled { return 1 } if ctx.isProgrammaticDismiss { @@ -1625,7 +1625,7 @@ public final class GtkBackend: AppBackend { ctx.onDismiss() } } - + } } diff --git a/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift b/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift index ccc66856ac..507a9984f8 100644 --- a/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift +++ b/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift @@ -23,7 +23,7 @@ public struct PreferenceValues: Sendable { /// The backgroundcolor of a sheet. Applies to enclosing sheets. public var presentationBackground: Color? - + /// Controls whether the user can interactively dismiss enclosing sheets. Applies to enclosing sheets. public var interactiveDismissDisabled: Bool? @@ -57,9 +57,10 @@ public struct PreferenceValues: Sendable { // For presentation modifiers, take the outer-most value (using child ordering to break ties). presentationDetents = children.compactMap { $0.presentationDetents }.first presentationCornerRadius = children.compactMap { $0.presentationCornerRadius }.first - presentationDragIndicatorVisibility = children.compactMap { - $0.presentationDragIndicatorVisibility - }.first + presentationDragIndicatorVisibility = + children.compactMap { + $0.presentationDragIndicatorVisibility + }.first presentationBackground = children.compactMap { $0.presentationBackground }.first interactiveDismissDisabled = children.compactMap { $0.interactiveDismissDisabled }.first } diff --git a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift index 4ba31e5e0b..b0265db740 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift @@ -35,16 +35,9 @@ struct SheetModifier: TypeSafeView { ) let bodyNode = AnyViewGraphNode(bodyViewGraphNode) - let sheetViewGraphNode = ViewGraphNode( - for: sheetContent(), - backend: backend, - environment: environment - ) - let sheetContentNode = AnyViewGraphNode(sheetViewGraphNode) - return SheetModifierViewChildren( childNode: bodyNode, - sheetContentNode: sheetContentNode, + sheetContentNode: nil, sheet: nil ) } @@ -72,8 +65,16 @@ struct SheetModifier: TypeSafeView { ) if isPresented.wrappedValue && children.sheet == nil { + let sheetViewGraphNode = ViewGraphNode( + for: sheetContent(), + backend: backend, + environment: environment + ) + let sheetContentNode = AnyViewGraphNode(sheetViewGraphNode) + children.sheetContentNode = sheetContentNode + let sheet = backend.createSheet( - content: children.sheetContentNode.widget.into() + content: children.sheetContentNode!.widget.into() ) let dismissAction = DismissAction(action: { [isPresented] in @@ -81,7 +82,7 @@ struct SheetModifier: TypeSafeView { }) let sheetEnvironment = environment.with(\.dismiss, dismissAction) - let dryRunResult = children.sheetContentNode.update( + let dryRunResult = children.sheetContentNode!.update( with: sheetContent(), proposedSize: backend.sizeOf(sheet), environment: sheetEnvironment, @@ -90,7 +91,7 @@ struct SheetModifier: TypeSafeView { let preferences = dryRunResult.preferences - let _ = children.sheetContentNode.update( + let _ = children.sheetContentNode!.update( with: sheetContent(), proposedSize: backend.sizeOf(sheet), environment: sheetEnvironment, @@ -99,7 +100,7 @@ struct SheetModifier: TypeSafeView { backend.updateSheet( sheet, - onDismiss: handleDismiss + onDismiss: { handleDismiss(children: children) } ) // MARK: Sheet Presentation Preferences @@ -137,13 +138,16 @@ struct SheetModifier: TypeSafeView { window: environment.window! as! Backend.Window ) children.sheet = nil + children.sheetContentNode = nil } return childResult } - func handleDismiss() { + func handleDismiss(children: Children) { onDismiss?() isPresented.wrappedValue = false + children.sheet = nil + children.sheetContentNode = nil } } @@ -153,16 +157,20 @@ class SheetModifierViewChildren: ViewGraphNodeC } var erasedNodes: [ErasedViewGraphNode] { - [ErasedViewGraphNode(wrapping: childNode), ErasedViewGraphNode(wrapping: sheetContentNode)] + var nodes: [ErasedViewGraphNode] = [ErasedViewGraphNode(wrapping: childNode)] + if let sheetContentNode = sheetContentNode { + nodes.append(ErasedViewGraphNode(wrapping: sheetContentNode)) + } + return nodes } var childNode: AnyViewGraphNode - var sheetContentNode: AnyViewGraphNode + var sheetContentNode: AnyViewGraphNode? var sheet: Any? init( childNode: AnyViewGraphNode, - sheetContentNode: AnyViewGraphNode, + sheetContentNode: AnyViewGraphNode?, sheet: Any? ) { self.childNode = childNode From 0447fa0533b6d02e5bb0f59d403f18bc57135607 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Tue, 21 Oct 2025 01:08:01 +0200 Subject: [PATCH 26/27] should fix AppKit and Gtk interactive dismissal --- Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift index b0265db740..7a895929ed 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift @@ -146,8 +146,6 @@ struct SheetModifier: TypeSafeView { func handleDismiss(children: Children) { onDismiss?() isPresented.wrappedValue = false - children.sheet = nil - children.sheetContentNode = nil } } From 2d97d73eb5f23239860cbc731cfbebeb7ef041ff Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Tue, 21 Oct 2025 01:14:23 +0200 Subject: [PATCH 27/27] using preferences from final result instead of dryRunResult --- .../Views/Modifiers/SheetModifier.swift | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift index 7a895929ed..62dbc9f80f 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/SheetModifier.swift @@ -82,16 +82,7 @@ struct SheetModifier: TypeSafeView { }) let sheetEnvironment = environment.with(\.dismiss, dismissAction) - let dryRunResult = children.sheetContentNode!.update( - with: sheetContent(), - proposedSize: backend.sizeOf(sheet), - environment: sheetEnvironment, - dryRun: true - ) - - let preferences = dryRunResult.preferences - - let _ = children.sheetContentNode!.update( + let result = children.sheetContentNode!.update( with: sheetContent(), proposedSize: backend.sizeOf(sheet), environment: sheetEnvironment, @@ -103,6 +94,8 @@ struct SheetModifier: TypeSafeView { onDismiss: { handleDismiss(children: children) } ) + let preferences = result.preferences + // MARK: Sheet Presentation Preferences if let cornerRadius = preferences.presentationCornerRadius { backend.setPresentationCornerRadius(of: sheet, to: cornerRadius)