Skip to content

Show cards in a system sheet on iOS 27+ - #33

Merged
nighthawk merged 35 commits into
mainfrom
feature/26329-system-sheets
Oct 14, 2026
Merged

nighthawk merged 35 commits into
mainfrom
feature/26329-system-sheets

Conversation

@nighthawk

@nighthawk nighthawk commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

Part of RM26329.

Why

On iOS 27, cards should be native Liquid Glass sheets, like Maps', with the system's dragging, snapping and scroll-to-expand. That makes them consistent with other apps, and leaves less hand-rolled pan machinery to maintain. Apps shouldn't need to change, and cards keep what they rely on: per-card map content, headers, paging, and a map that stays interactive at every height.

What

  • presentationStyle, defaulting to .automatic: on iOS 27+, the cards move into a UISheetPresentationController. It's set up once and adapts to the size classes by itself: at the bottom in compact width, and 400pt wide on the leading edge otherwise, e.g., on iPad and on an iPhone in landscape. iOS 26 and earlier, Mac Catalyst, visionOS and sidebar mode keep the classic card; .classic opts out. The sheet placement API needs Xcode 27 to build.
  • An invisible placeholder follows the sheet's top edge, so map insets, map buttons and the header work as before.
  • The sheet is the controller's presentedViewController. Use the new presentedOverlayViewController and cardOverlayView instead; present and dismiss on the controller route around the sheet.
  • iPhone Duo (iOS 27.1 SDK): the close button, page chevrons and the new TGCard.verticalBarActions go into the vertical bar, like Mail's.
  • TGCardStyle.expandedBackgroundColor fades in behind raised cards in the sheet. The classic card keeps its look.
  • Requires iOS 16, up from 13.

It also fixes the Example CI job. After merging, tag 2.6.0, which skedgo/tripkit-ios needs.

Testing

In the Example app:

  1. On an iPhone simulator with iOS 27: cards show in a glass sheet with three heights. The map stays tappable at every height, pushing and popping cross-fades, "Show Pages" pages within the sheet, and a card's header shows above the sheet.
  2. Rotate to landscape and back: the sheet moves to the leading edge and back to the bottom.
  3. On iPad with iOS 27: the sheet is on the leading edge. On iOS 26, and with the -classicCards launch argument: the classic card, unchanged.
  4. On the iPhone Duo simulator (iOS 27.1, portrait), open "Show Pages": the close button and chevrons sit at the top of the vertical bar, and star and share at its bottom, at every sheet height. The star toggles.

🤖 Generated with Claude Code

@nighthawk

nighthawk commented Oct 9, 2026 •

Copy link
Copy Markdown
Member Author

Suggested squash-merge commit message:

feat: show cards in a system sheet on iOS 27+

With the default `presentationStyle` of `.automatic`, cards now show in
a `UISheetPresentationController` on iOS 27+. It adapts to the size
classes by itself, like Maps': at the bottom in compact width, and on
the leading edge otherwise, e.g., on iPad and on an iPhone in
landscape. iOS 26 and earlier, Mac Catalyst, visionOS and sidebar mode
keep the classic card, and `.classic` opts out. Map content, headers,
paging and map buttons work as before, and the map stays interactive at
every sheet height. A header spans the top of the screen above the
sheet in compact width, and floats right above it, aligned with it,
when the sheet is on the side.

As the sheet is the controller's `presentedViewController`, use the new
`presentedOverlayViewController` and `cardOverlayView` instead. Present
and dismiss on the controller route around the sheet.

On devices with a vertical bar, e.g., the iPhone Duo, the close button,
page chevrons and the new `TGCard.verticalBarActions` go into that bar.
Cards span the sheet under the bar, keeping titles and contents within
the safe area, so that only backgrounds and the scroll indicator extend
under it. When collapsed, the title and the bottom map buttons use the
full width, and the bar's actions are hidden. The new
`TGCardStyle.expandedBackgroundColor` fades in behind raised cards in
the sheet.

Pages of a page card are now hosted in child view controllers, so that
they get the safe area and layout margins, which also gives inset
grouped tables on pages their margins back.

Also requires iOS 16, and fixes the Example CI job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Comment thread Sources/TGCardViewController/cards/TGCard.swift Outdated
Comment thread Sources/TGCardViewController/TGCardViewController+SystemSheet.swift Outdated
Comment thread Sources/TGCardViewController/TGCardViewController+SystemSheet.swift
Comment thread Sources/TGCardViewController/TGSheetHostViewController.swift Outdated
nighthawk and others added 19 commits October 9, 2026 19:46
Xcode 27 no longer builds for iOS 13, and the system sheet work needs
iOS 16 APIs (custom detents). TripKitUI already requires iOS 16, so
consumers are unaffected. Bumps tools-version to 5.7 for `.v16`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds `presentationStyle` (`.automatic`, `.classic`, `.systemSheet`). With
`.automatic`, on iOS 26+ in compact width and regular height, the card
stack moves into a `UISheetPresentationController` so cards get the
system's floating glass sheet, dragging and scroll-to-expand. Regular
width, compact height, Catalyst, sidebar mode and controllers that are
themselves presented in a sheet keep the classic presentation, and the
controller switches back and forth as size classes change.

Only the cards move into the sheet. The card wrapper stays behind as an
invisible placeholder that mirrors the sheet's top edge on every layout
pass, so map buttons, map insets, the header and map managers work as
before. The three card positions map to custom detents, the extended one
leaving room for a header. The sheet never dims, so the map and header
stay interactive.

Since the sheet is this controller's `presentedViewController`, `present`
and `dismiss` are routed to whatever is on top of the sheet, and
appearance callbacks for full screen presentations are forwarded. New
`presentedOverlayViewController` and `cardOverlayView` help consumers
that relied on `presentedViewController` or on adding views to `view`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UIKit's own present and dismiss are lenient for callers that aren't on
the main actor yet, while the new overrides weren't, which broke such
callers in TripGo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cards below the top card used to stay in the view hierarchy with an alpha
of 0, so every card in the stack kept taking part in layout, rendering and
accessibility. Once a push has finished, the covered card's view is now
removed from its superview and only added back when the card above it
gets popped. The views stay alive, so cards keep their state, such as
scroll positions and bindings set up in `didBuild`.

SwiftUI views in covered cards now get `onDisappear` and their `.task`s
get cancelled, and both run again when the card is revealed.

Also no longer hides a card that was popped back to before the push of the
card above it had finished.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The system sheet rounds its background but doesn't clip its content, so
opaque card content, like TripKitUI's grouped table cells, stuck out past
the sheet's bottom corners. Clips the sheet's view with a concentric
corner radius.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The classic slide-up-and-bounce suggests a new card landing on top of the
old one, which fits the classic presentation, but looks odd inside a
sheet that's already there. In a system sheet, cards now cross-fade in
place instead, while the sheet animates to the new card's detent.

Also keeps swiping from the screen's edge to pop working on the map in a
system sheet, and adds it to the sheet itself, too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
iOS 27.1 no longer launches apps that don't support scenes, so the
Example app crashed on launch on the iPhone Duo simulator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On devices with a vertical bar, like the iPhone Duo's outer display, the
system puts a sheet's close button into that bar, below the status bar
once the sheet is at full height. Cards have their close buttons in their
titles, which then ended up under the status bar, or in a different spot
depending on whether the title view respected the safe area.

While the cards are in a system sheet next to a vertical bar, the close
buttons in the top card's titles are now hidden (keeping their layout),
and a stand-in close button is pinned to the sheet's bar layout region
(`layoutGuide(for: .bar(onEdge:extent:))`, iOS 27.1), which forwards taps
to the card's own button. It follows the bar as the sheet changes height,
when the device folds, and when switching pages of a page card.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On devices with a vertical bar, such as the iPhone Duo's outer display,
the top card's items now go where the system puts a navigation bar's:
the close button and, for page cards, previous/next buttons at the top
of the bar, and the card's new `barActions` grouped at the bottom, all
icon-only. They stay in the bar at every height of the sheet: one copy
sits over the map and another on the sheet, each showing where it's in
front.

Cards can observe `showsBarActions` to leave those actions out of their
own content while they're in the bar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cards in the sheet now stay within its safe area horizontally. On the
iPhone Duo, that's what keeps content out from under the vertical bar
and its items, which the sheet reserves at every height.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When collapsed, the sheet now leaves as much space below the title's
content as there is above it, where the grabber sits. That's measured
from what the title draws, as title views have their own margins, e.g.,
the home card's search bar sits close to the grab handle. It applies
while the sheet is small enough to float. Taller sheets attach to the
bottom edge, where UIKit adds the bottom safe area below the title, so
those keep ending where the header does.

Like classic cards, the content now also fades out towards the collapsed
height, so that nothing below the title peeks into the sheet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`TGCardStyle.expandedBackgroundColor` fades in behind the whole card as
it moves up from collapsed, along with its content, and fully shows
when it's peaking or extended. With a clear `backgroundColor` on iOS
26, that keeps the map from shining through the content of a raised
card, while a collapsed card still shows its material. Applies to both
system sheets and classic cards; not on Mac Catalyst.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In a system sheet, the cards stay within the sheet's safe area, which
leaves out the column next to a vertical bar, e.g., on the iPhone Duo.
The expanded background now goes on the sheet itself, so that it also
covers that column, rather than leaving a strip of glass next to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The vertical bar APIs for the iPhone Duo come with the iOS 27.1 SDK.
With older Xcodes, the cards keep their own close buttons and actions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The runner no longer has the named simulator, so the job failed before
building anything.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
visionOS has no sheet detents, and neither visionOS nor Mac Catalyst
have the vertical bar APIs. Cards never use a system sheet there, so
leave those parts out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`TGCard.barActions` is now `verticalBarActions`, and `showsBarActions`
is `showsVerticalBarActions`, which says where they show. Internal names
follow suit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The classic card keeps its own look, including its rounded corners, so
`TGCardStyle.expandedBackgroundColor` now only applies to the sheet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nighthawk
nighthawk force-pushed the feature/26329-system-sheets branch from eaa5ef4 to 7ef0e5a Compare October 9, 2026 08:46
@nighthawk nighthawk self-assigned this Oct 9, 2026
@nighthawk nighthawk added the enhancement New feature or request label Oct 9, 2026
@nighthawk
nighthawk requested a review from fersagui October 9, 2026 08:48
nighthawk and others added 2 commits October 9, 2026 20:15
The sheet's safe area covers the vertical bar's column when the bar is
on the trailing edge, but not when it's on the leading edge, e.g., on an
unfolded iPhone Duo with the app on the left screen. The cards in the
sheet now get inset by the bar's column on whichever side it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On iOS 27, the sheet is set up once to adapt to the size classes by
itself, like Maps': at the bottom and full width in compact width, and
400pt wide on the leading edge otherwise, e.g., on iPad and on an iPhone
in landscape. As the sheet no longer needs to make way for the classic
card when the size classes change, the controller no longer switches
between the two on rotation or resizing.

That needs the sheet placement API of iOS 27, so system sheets are now
iOS 27+ only, and `PresentationStyle.systemSheet`, which forced them on
earlier versions, is gone. iOS 26 and earlier keep the classic card.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nighthawk nighthawk changed the title Show cards in a system sheet on iOS 26+ Show cards in a system sheet on iOS 27+ Oct 9, 2026
UIKit extends a sheet underneath the vertical bar of an iPhone Duo,
which can look bulgy next to the bar's items. With the new
`sheetAvoidsVerticalBar`, the view that UIKit lays out the sheet in is
narrowed to the space beside the bar, so the sheet sizes itself to that,
and the bar's items always sit over the map. The Example app opts in
with the `-avoidVerticalBar` launch argument.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nighthawk and others added 13 commits October 11, 2026 19:46
`sheetAvoidsVerticalBar` now defaults to `true`. Set it to `false` to
let the sheet extend underneath the bar, as UIKit does by itself. The
Example app's launch argument for that is now `-sheetUnderVerticalBar`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drops `sheetAvoidsVerticalBar`, going back to UIKit's own sheet layout
next to the vertical bar of an iPhone Duo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the sheet is collapsed, it only shows the card's title, which now
uses the sheet's full width rather than staying clear of the vertical
bar, and the card's actions aren't shown in the bar. They fade in with
the card's content as the sheet rises towards peaking.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While the sheet is collapsed next to a vertical bar on the trailing
edge, the bottom map buttons now ignore the bar's safe area inset, so
they line up with the collapsed sheet below them, rather than stopping
short of the bar. Above that, they stay clear of the bar as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The full-width content and the bottom floating views ignoring the bar
only kicked in when the top card had vertical bar items, so cards
without any (e.g. TripGo's home card) kept the bar inset when collapsed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
iOS 27 floats taller collapsed sheets a few points above the bottom of
the screen, so measuring from the screen's bottom left them slightly
"expanded": vertical bar actions stayed faintly visible and the
collapsed layout didn't kick in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cards in a system sheet now span its full width, with the sheet host's
safe area covering the vertical bar's column, so that only backgrounds
and the scroll indicator go under the bar, like in Apple's apps. Titles
and plain card content stay within the safe area, as do cell contents.

Pages of a page card are hosted in child view controllers, as UIKit only
passes a scroll view's safe area and layout margins on to its content at
the edges of that content. This also gives inset grouped tables on pages
their margins back, e.g., in the trip overview, in both presentations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sheet's safe area and the cards' constraints are updated while the
sheet lays out, after the cards in it are laid out already. When that
was the last layout pass, e.g., when the sheet settled after a drag,
the cards kept the previous inset, such as reserving the bar's column
while collapsed, until the next drag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In compact width, the header above a system sheet now spans the full
width of the screen with square corners, like a toolbar, rather than a
rounded panel the width of the card. Its content gets some more room
around it, and stays within the safe area, e.g., clear of a vertical bar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the system sheet is on the side of the screen, e.g., on iPad or an
unfolded iPhone, the header now floats right above it as a matching
rounded panel: aligned with the sheet's edges, as far from the top as
the sheet is from the side, and with the same room around its content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The header above a system sheet now shows the top card's
`expandedBackgroundColor` behind its content, fading in and out with
the sheet's height exactly like the sheet does, so the header always
matches the card below it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A header that floats above a system sheet on the side now has top
corners concentric with the screen's, rather than a fixed radius that
clashes with the display's rounded corners.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hosted pages of a page card get the system's safe area, but not
what the sheet adds for a vertical bar on the leading edge, e.g., when
the app is on the left screen of an unfolded iPhone Duo. Only the first
page got it, through the pager's edge. Each page's host now makes up
the difference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@fersagui fersagui left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. My AI found these improvements:

[P1] The compiler guard enables iOS 27.1 vertical-bar APIs absent from the 27.0 SDK. Confirmed by building the Example app.
[P2] Side sheets receive bottom map padding. On iPad and landscape iPhone, map managers receive zero horizontal padding despite the leading sheet, so fitted map content can remain obscured.

@nighthawk
nighthawk merged commit 1934e88 into main Oct 14, 2026
3 checks passed
@nighthawk
nighthawk deleted the feature/26329-system-sheets branch October 14, 2026 23:19
nighthawk added a commit to skedgo/tripkit-ios that referenced this pull request Oct 19, 2026
TripKitUI uses API from skedgo/TGCardViewController#33. Switch back to a
version requirement, from 2.6.0, once that's tagged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants