Skip to content

DON-3911: BPKBottomSheet's modal style as a native sheet - #2694

Draft
Soheil Novinfard (novinfard) wants to merge 5 commits into
mainfrom
donburi/DON-3911-native-bottom-sheet-poc
Draft

Soheil Novinfard (novinfard) wants to merge 5 commits into
mainfrom
donburi/DON-3911-native-bottom-sheet-poc

Conversation

@novinfard

@novinfard Soheil Novinfard (novinfard) commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

BPKBottomSheet's modal style can now be presented as a native UISheetPresentationController page sheet. It's behind a new BpkConfiguration config, .nativeBottomSheet, and off by default, so nothing changes until an app turns it on. The persistent style keeps the floating panel, because it sits inside its parent rather than being presented.

Draft until design agrees the look.

Remember to include the following changes:

  • README.md: new section "Native sheet (behind configuration)"
  • Tests: 17 new unit tests (13 for the bottom sheet, 4 for the configuration)
  • Screenshotting code: not changed; the screenshots use the persistent style
  • Adding a component? Remember to expose it in the main Backpack.h header file: not needed, since the public API of the bottom sheet doesn't change

Why

The floating panel draws its own surface, grabber and backdrop, and works out its own size, so it misses the window adaptation UIKit gives a native sheet. Every new window shape needs code here.

On a foldable, a sheet opened unfolded keeps its unfolded width after folding, centred on the smaller display, so its content is cut off on the left. Rotating a phone resizes it correctly; only the fold shows it. #2687 works around the panel's size with a readable-width cap (672 pt) and a 60% cap on the half and tip positions, and the 60% cap drops tall sheets when the text size changes with a sheet open.

A native sheet is laid out again by UIKit whenever the window changes, and on a wide window it's a centred card, with no code here.

The switch

try BpkConfiguration.shared.set(configs: [.nativeBottomSheet])
  • Off by default. With it off, both styles use the floating panel exactly as today.
  • Not part of .all, so it can be tested on its own.
  • Read when a bottom sheet is created.

How the API maps

BPKBottomSheet Native sheet
half position Custom detent at the half inset's height (386 pt by default). Left out when it reaches the tallest height
full position The system's large detent. The full and tip insets aren't used
No scroll view One detent at the content's fitting height, plus the bottom section
scrollViewToTrack setContentScrollView(_:for:), so dragging the list at its top grows or shrinks the sheet
bottomSectionViewController Pinned to the sheet's bottom with the same top shadow; the tracked scroll view gets a matching bottom inset
move(to:) Sets the selected detent, animated; .hidden dismisses
updateLayout() invalidateDetents()
onDismissed Called when the sheet is dismissed
delegate.bottomSheetDidChangePosition Called when the selected detent changes
Look System grabber, BPKCornerRadiusLg corners, surfaceElevatedColor background

The sheet keeps its BPKBottomSheet alive until it's dismissed, because callers that only present viewControllerToPresent often don't hold on to it. The content and bottom section views are loaded when the bottom sheet is created, as the floating panel did, because callers set state on them straight after.

Differences to agree

  • Look. The grabber and the dimming are the system's. On iOS 26 a sheet that isn't full height floats in from the screen edges. On a wide window the sheet is a centred card, not a full-width panel. On iOS 18 there is no visible change.
  • tip. A modal sheet has no tip detent, so move(to: .tip) goes to half.
  • Height. UIKit sets the width and the tallest height. The full position is always the system's large height.
  • Gestures. Drag to dismiss and tap outside to dismiss come from UIKit.

Checked

  • The 17 new unit tests and the existing bottom sheet tests pass in CI.
  • In a production app built against this branch, one build with the config off and on (iPhone, iOS 26.3): off shows the floating panel, on shows the native sheet.
  • With the native sheet, in the same app: list sheets, content-fit sheets, a fixed-height sheet moved to full, and a sheet with a bottom section, on iPhone (iOS 26.3 and iOS 18.0) and on the iPhone Duo simulator folded, unfolded and half folded. Folding and unfolding with a sheet open resizes it each time. The Duo runs were made before the config was added, with the native path always on.
  • Changing the text size with a tall sheet open keeps its height. The floating panel drops to 60%.
  • iPad in portrait.

Not checked yet

  • A physical device.
  • VoiceOver. The accessibility hierarchy shows the system grabber with a name and a value, but it hasn't been tried with VoiceOver running.
  • iPad in landscape.

The modal style is now a UISheetPresentationController page sheet, so UIKit
sizes and places it for the window and lays it out again when the window
changes, for example when a foldable folds or unfolds. The half and full
positions map to custom detents, a sheet without a scroll view fits its
content, the tracked scroll view drives the sheet, and the bottom section
stays pinned. The public API is unchanged. The persistent style isn't
presented, so it keeps the floating panel.

Co-authored-by: OpenCode <noreply@opencode.ai>
@novinfard Soheil Novinfard (novinfard) added minor Non breaking change poc Proof of concept labels Sep 30, 2026
Base automatically changed from donburi/DON-3879-iphone-duo-fixes to main September 30, 2026 11:35
…ve-bottom-sheet-poc

Co-authored-by: OpenCode <noreply@opencode.ai>
@novinfard Soheil Novinfard (novinfard) changed the title DON-3911: [PoC] BPKBottomSheet's modal style as a native sheet DON-3911: BPKBottomSheet's modal style as a native sheet Oct 1, 2026
Three differences showed up when the native sheet replaced the floating
panel for the modal style:

- The content and bottom section views were loaded when the sheet was
  presented, not when the bottom sheet was created. State set on them
  straight after creation could be lost. They are loaded on creation
  again.
- A full position with no top inset was a custom height, so tall sheets
  came out shorter and detached from the screen edges. It now uses the
  system's large height, and a half height that reaches the tallest
  height leaves only the full position.
- The sheet stayed attached to the edges in a compact-height window, so
  on a wide, short window it ran the full width. It now follows the
  system's default and is a centred card there.

Co-authored-by: OpenCode <noreply@opencode.ai>
The modal style only uses the half inset. Its full position took the
full inset off the tallest height, so a sheet whose half height already
reached the top came out shorter than with the floating panel. The full
position is now always the system's large height.

Co-authored-by: OpenCode <noreply@opencode.ai>
The modal bottom sheet keeps the floating panel by default. The native
sheet is used when BpkConfiguration is set with the new
nativeBottomSheet config, so the two can be compared in an experiment.
The config is not part of `all`, which stays the visual refresh set.

Co-authored-by: OpenCode <noreply@opencode.ai>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

minor Non breaking change poc Proof of concept

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant