fix(client/apple): the tray blur keeps its shape, and the tab pill turns to glass
ci / bun-nix (pull_request) Successful in 27s
ci / docs-site (pull_request) Successful in 1m4s
ci / web (pull_request) Successful in 1m9s
ci / rust-arm64 (pull_request) Successful in 1m30s
apple / swift (pull_request) Successful in 1m35s
apple / screenshots (pull_request) Skipped
ci / rust (pull_request) Canceled after 4m39s

Two follow-ups from the second on-glass pass:

- The tray blur's pop-in survived the geometryGroup: the full-bleed
  growth (negative padding + safe-area expansion) rode the push's
  transaction, and safe-area resolution sits outside a geometry group.
  The scrim now pins its own geometry out of any animation - the layer
  fade/slide still carries it, only its SHAPE can never animate. The
  bottom overshoot grows 32 -> 72 pt (the tray sits over scrolling rows
  plus the detail line; the blur influence starts well above the legend
  now) and the mask holds strength longer before dissolving.

- The selected tab pill is a Liquid Glass surface (accent-tinted
  through consoleGlass, material fallback pre-26/tvOS) - the strip
  wears the same material language as the rows below it. The
  matched-geometry travel between pills is unchanged.
This commit is contained in:
2026-08-07 14:45:15 +02:00
parent 49fa344c92
commit f674a06a08
3 changed files with 20 additions and 10 deletions
@@ -385,15 +385,23 @@ struct GamepadTrayScrim: View {
LinearGradient(
stops: [
.init(color: .black, location: 0),
.init(color: .black.opacity(0.9), location: 0.5),
.init(color: .black.opacity(0.92), location: 0.55),
.init(color: .clear, location: 1),
],
startPoint: fromEdge, endPoint: toContent)
}
// Grow past the tray so the fade-to-clear happens OUTSIDE its bounds the tray's own
// text always sits on the strong part, rows blur out before they reach it.
.padding(edge == .top ? .bottom : .top, -32)
// text always sits on the strong part, rows blur out before they reach it. The bottom
// gets the longer runway: its tray sits over SCROLLING rows plus the detail line, and
// the field verdict on the short reach was rows colliding visibly with the legend.
.padding(edge == .top ? .bottom : .top, edge == .top ? -44 : -72)
.ignoresSafeArea()
// This view's geometry must NEVER animate: mounted inside a pushed shell layer, the
// full-bleed growth above (the negative padding and the safe-area expansion) rode the
// push's transaction, so the blur visibly entered at content bounds and grew into
// place and `.geometryGroup()` on the layer does not reach safe-area resolution.
// The layer's own fade/slide still carries the scrim; only its SHAPE is pinned.
.transaction { $0.animation = nil }
}
}
@@ -124,10 +124,10 @@ struct GamepadHomeView: View {
#if os(iOS)
if let screen = topScreen {
screenLayer(screen)
// Resolve the screen's internal layout (safe-area trays, the scrims'
// `.ignoresSafeArea()` full-bleed) BEFORE the insertion animates: without
// this the tray blurs entered at their content bounds and visibly grew to
// the screen edges mid-push.
// Settle the screen's internal layout before the insertion animates, so
// descendants never lerp from a half-resolved first frame. (Not sufficient
// for the tray blurs on its own safe-area expansion resolves outside a
// geometry group; GamepadTrayScrim pins its own geometry too.)
.geometryGroup()
.zIndex(1)
.id(screen.id)
@@ -257,10 +257,12 @@ struct GamepadSettingsView: View {
.padding(.vertical, 7)
.background {
// One shared capsule that MOVES between pills, rather than one per pill fading
// in and out the highlight travels the way the press did.
// in and out the highlight travels the way the press did. A Liquid Glass
// surface (accent-tinted through consoleGlass), so the strip wears the same
// material language as the rows it sits above.
if selected {
Capsule()
.fill(ink.accent(0.85))
Color.clear
.consoleGlass(Capsule(), tint: ink.accent(0.85))
.matchedGeometryEffect(id: "tab", in: tabHighlight)
}
}