Skip to content

Conversation

@stackotter
Copy link
Owner

@stackotter stackotter commented Nov 27, 2025

As per #219;

Is it already Christmas? No but I brought you a gift anyway.

Jokes aside.

This PR adds the .sheet Modifier, like we know it from SwiftUI with isPresented Binding and optional onDismiss callback.

Also some sheet modifiers:
presentationBackground
presentationDragIndicatorVisibility
presentationCornerRadius
presentationDetents
interactiveDismissDisabled

and Example usage in WindowingExample

support for the modifiers varies quite a bit. most are ignored if unsupported, interactiveDismissDisabled and presentationBackground are required as both shouldn’t be limited by Framework features.

Currently only UIKitBackend, AppKitBackend and GtkBackend support sheets, as I sadly couldn’t get them to work properly on WinUI. I hope someone else can build on the Foundation.

As was to expect, there are some weird quirks:
on UIKitBackend and AppKitBackend sheet content is placed top-leading. On GtkBackend on Linux Text elements are very small without further modification instead of allocation space. Same thing happened on macOS with GtkBackend in the 2nd and 3rd window (before and after my changes).

I expect those to be fixable through SCUI Frontend Code. If you know how to fix those in the Backends, I’d be happy to do so before a merge.


In addition to #219, this PR includes some of my own changes to apply PR feedback and ensure consistent behaviour across platforms;

  • Updated the sheet APIs to avoid Any.
  • Audited the backend implementations to ensure behaviour regarding the onDismiss handlers of implicitly dismissed nested sheets. I've decided that they should be called even though the parent sheet was dismissed programmatically. I also noticed that many backends trigger dismissals on nested sheets automatically so we could eliminate the need for keeping references to child sheets in said backends.
  • Audited the backend implementations to ensure consistent onDismiss ordering. onDismiss now gets called after the sheet gets dismissed, across all backends (AppKitBackend was an odd one out).
  • Merged presentation preference setting into updateSheet because I realised that most of the setPresentation... AppBackend methods should still be called in the case of a nil value to properly reset their corresponding backend property back to its default value (rather than retaining their previous non-nil backend). This made it make sense to merge the setPresentation methods into updateSheet because they were always all called at in succession.
  • Fixed issue in windowing example where dismissing the 5 second sheet interactively didn't cancel the associated 5 second task, so if you reopened the sheet within 5 seconds, it would get cancelled sooner than 5 seconds.
  • Updated the windowing example to allow infinite nesting of sheets, and updated 'dismiss parent sheet' to 'dismiss root sheet' because that makes more sense with infinite nesting.
  • Addressed the comments from my review.
  • And probably a few other things I missed in this summary
  • Centered sheet contents horizontally on iOS
  • Handled the case of an empty detents array on iOS (now defaults to [.large]. Without this, UIKit crashes.
  • Fixed nested iOS sheet dismissal (possibly an issue introduced by my code). We need to call dismiss twice when there's a nested sheet, because the first time it dismisses the nested sheet, and the next time it defers to the parent to dismiss the sheet itself. This is because the presenting controller is the one responsible for dismissing presented controllers.
  • Added a default sheet background colour on iOS (otherwise it's transparent and looks really weird).
  • Updated UIKitBackend's interpretation of automatic drag indicator visibility to show the indicator when there is more than one detent.
  • Renamed showSheet to presentSheet
  • Escape key dismisses sheets under Mac Catalyst (and probably iPadOS)

Platform consistency summary

  • onDismiss gets called after the backend has dismissed the sheet and before SwiftCrossUI has handled the dismissal (i.e. the sheet's associated binding will still be set to true).
  • onDismiss doesn't trigger backend.dismissSheet.
  • Dismissing a sheet containing nested presentations causes the nested presentations to be dismissed, and importantly, triggers their corresponding onDismiss handlers. Nested presentations are dismissed from the topmost down.

@stackotter stackotter added the feature A feature request label Nov 27, 2025
MiaKoring and others added 4 commits December 3, 2025 09:48
* Sheet presentation and nesting added to AppKitBackend

* Added basic UIKit Sheet functionality and preparation for presentation modification

* added presentationDetents and Radius to UIKitBackend

* improved sheet rendering

* added presentationDragIndicatorVisibility modifier

* added presentationBackground

* UIKit sheet parent dismissals now dismiss both all children and the parent sheet itself

on AppKit its probably easier to let users handle this by just setting isPresented to false, as the implementation is quite different than on UIKit using windows instead of views. Maybe potential improvement later/in a different pr

* added interactiveDismissDisabled modifier

* renamed size Parameter of SheetImplementation Protocol to sheetSize

* GtkBackend Sheets save

* finished GtkBackendSheets

* documentation improvements

* Should fix UIKitBackend compile issue with visionOS and AppBackend Conformance of WinUIBackend and Gtk3Backend

* maybe ease gh actions gtk compile fix?

* fixed winUI AppBackend Conformance

* removed SheetImplementation Protocol, replaced with backend.sizeOf(_:)

changed comment in appbackend

* Adding sheet content in createSheet() instead of update (UIKit, AppKit)

* adding sheet content on create (Gtk)

* comment improvements

* maybe part of letting scui dictate size?

* changes

dismissSheet now expects non optional Window
rolled back proposed size try

* moved signals to Gtk/Widgets/Window

* removed now unecessary old code

* mostly comment changes

* onAppear now get called correctly, onDisappear only on interactive dismiss

No idea why it doesn't get called on programmatic dismissal. Neither like it is now nor through a completion handler of the programmatic dismiss

* should fix AppKit and Gtk interactive dismissal

* using preferences from final result instead of dryRunResult

* Fixed sheet rendering with GtkBackend on Linux

* UIKit Sheet changes

* Window uses EventControllerKey generated class instead of c interop

* Replaced Window Escape key press c interop with generated class

* Formatting and comments

* Trying GdkModifierType instead of UInt

* Made setPresentationBackground save to run/update n times on AppKitBackend

* Fixed parent sheet dismissal leaves child sheet behind (GtkBackend testing pending)

* beginCriticalSheet replaced with beginSheet on AppKitBackend

* fix tvOS (and visionOS) compilation error

* removed .formSheet modalPresentationStyle for tvOS 26 due to CI failure
@stackotter stackotter merged commit 7406f22 into main Dec 3, 2025
22 checks passed
@stackotter stackotter deleted the sheets branch December 3, 2025 00:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature A feature request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants