Repository navigation
iPhone Duo layout: fold-aware player, queue beside it, swipe-down, and fixes - #5
Merged
Merged
Conversation
The spec, plan and probe screenshots live in the gitignored docs/superpowers/ inside this worktree, so the decisions behind them (scope, 26.1 floor, CI on Xcode 27.1+, player-alone upright) and the Phase 0 findings go here, where a worktree removal can't take them. Also corrects the toolchain note: xcode-select is now Xcode 27.2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VriHZgxfrBNm1js1UNLYTN
The iPhone Duo work adopts ArrangementView behind `if #available(iOS 27.1, *)`. A missing symbol is a property of the SDK, not the deployment target, so #available does not help on macos-26's Xcode 26.6. Move to the `xcode-27` runner (the only GitHub-hosted label with Xcode 27.x; still a preview image, actions/runner-images#14404) and pin Xcode_27.1.app via DEVELOPER_DIR (GA 27A9275 on every image version; Xcode_27.2.app is still the RC on part of the fleet). A new assertion step fails the job up front if the image ever drops or downgrades it. The iOS deployment floor stays 26.1. Unverified until the first push. Source: https://github.com/actions/runner-images/blob/main/images/macos/xcode-27-arm64-Readme.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VriHZgxfrBNm1js1UNLYTN
…ement
On iPhone Duo's outer display the 27.1+ SDK lays the tab bar out
vertically, so tabViewBottomAccessory is ~290pt wide while its placement
still reads .expanded. All five transport controls plus the chevron then
left the title 34pt ("Aw…"). PillLayout chose controls by Slot alone and
never used textBudget to shed any.
PillLayout.controls(slot:width:hasLeadingSlot:trailingReserve:) now drops
the skip intervals together, then previous, until the text column has
minTextWidth (90pt); play/pause and next are never shed. An unmeasured
width returns the full set, so the iPhone 17 Pro's 360pt pill (104pt of
text) keeps all five. NowPlayingPill measures its outer width with
onGeometryChange and never animates a control in or out.
9 new PillLayoutTests (140 total, all passing).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VriHZgxfrBNm1js1UNLYTN
StandardNowPlayingScreen's no-art branch was a rigid 280×280 MonogramTile. On iPhone Duo's landscape inner display (~951×669pt) it pushed the header (dismiss) and bottom row (AirPlay, queue, more) off screen in Soundboard, Tape Room and Shoebox. It also overflowed on a portrait iPhone: the stack spilled ~15pt past its vertical padding at each end and truncated the venue line to one row. The monogram now takes min(width, height) of its box, capped at the old 280pt (ideal 280 for unconstrained proposals) — the same flexibility the real-artwork Image already had. On iPhone 17 Pro the placeholder now lays out exactly where real artwork does (verified pixel-for-pixel against a real CDN cover); on the Duo inner display the whole player fits, art ~104-128pt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VriHZgxfrBNm1js1UNLYTN
Same overflow as 8ea1533, in the Click Wheel theme: a rigid 260pt MonogramTile plus a fixed 330×260 satellite cluster pushed the header off the top of iPhone Duo's landscape inner display and cut the ±15/30 satellites off the bottom. On a portrait iPhone 17 Pro it overflowed too, by ~16pt at each end (header pinned to the safe-area edge). - The monogram scales with its box, capped at its old 260pt. - The control pad scales as a unit to the available width via the new pure NowPlayingFit.scale(available:natural:) — scaleEffect plus a matching frame, since scaleEffect alone keeps the 330pt footprint. Proportions never reflow. Scale is 1 on iPhone and the Duo today; it exists for split columns / Split View narrower than 330pt. NowPlayingFit is Foundation-only and compiled into AppleNugsTests (6 new tests, 146 total). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VriHZgxfrBNm1js1UNLYTN
- nowPlayingQueueBeside (environment, default false): when true, each full-screen player swaps its queue button for a same-size, hidden placeholder — never removed, so the fixed-geometry rows keep their shape. The sheet stays attached. - DashboardPanel(showsNowPlaying:) (default true): beside the player the panel's "Now" section is redundant. Static per instance; every existing call site is unchanged. - nowPlayingReadableWidth(): on iOS at regular width the player's content caps at 520pt, centered, background still full-bleed. Chosen by value, not if/else, so a size-class flip can't reset the players' @State. A no-op on macOS (whose Now Playing window shares these views) and on compact iPhones. No behavior change until the next commit sets the flag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VriHZgxfrBNm1js1UNLYTN
…ueue The full-screen cover now hosts NowPlayingArrangement: on iOS 27.1+ an ArrangementView with the player primary and DashboardPanel (Signal + Up Next, no "Now" section) secondary, split horizontally only. Where it can't split — every other iPhone, the Duo's compact outer display, the inner display held upright — the SDK shows the player alone, exactly as before. Older iOS takes the bare player (the floor stays 26.1). Measured on the Duo simulator, unfolded flat: an even 50/50 split of the safe area, two 433.5×635pt columns, each reporting a compact horizontal size class. The player's own queue button hides when the queue column appears (derived from the secondary's onAppear — the SDK has no split visibility signal). iPhone 17 Pro / iOS 26.5 output is pixel-identical. Unverified: whether the secondary is removed (vs. kept at zero size) when the inner display can't split — needs a fold or an upright pose. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VriHZgxfrBNm1js1UNLYTN
- A playback error stayed invisible on the Duo's split player: on iOS the error only rendered in DashboardPanel's "Now" section, which the queue column drops, and the player's queue button (the old route to it) is hidden there. The panel now keeps the error label on its own when showsNowPlaying is false. - The queue-beside flag counts live secondary instances instead of flipping one Bool, so a rebuilt secondary whose onAppear lands before the old one's onDisappear can't leave the flag false with the column on screen. - PillLayout tests pin the inclusive 90pt floor at its exact edges (346 vs 345 with the chevron, 314 vs 313 without) and the no-leading-slot case. 149 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VriHZgxfrBNm1js1UNLYTN
Pick-up-here now lists every commit on the branch, the measured split geometry, and exactly what is still unverified (outer-display pill, fold/unfold, upright, book pose, Split View, real art on a real Duo, CI). Conventions gain the three rules that fail silently if broken: now-playing art yields height, the pill sheds by measured width, and the queue-beside placeholder contract. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VriHZgxfrBNm1js1UNLYTN
…can't split Tim folded and unfolded the Duo simulator with the split player open: the outer display shows the player alone with its queue button back (state kept), unfolding restores the split, and the folded tab root's pill reads "Away From th…" with prev/play/next instead of "Aw…". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VriHZgxfrBNm1js1UNLYTN
Developer Mode, building against the specific device id with -allowProvisioningDeviceRegistration (a generic build never registers a new phone), and a pending Program License Agreement that blocks registration until accepted. devicectl, not xctrace, is the reliable view of the phone's state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VriHZgxfrBNm1js1UNLYTN
The player has always been a plain fullScreenCover, which iOS doesn't let you swipe away — only the header chevron closed it, on every iPhone. A vertical drag on the PLAYER (never the queue column, whose ScrollView keeps scrolling) now moves the whole arrangement down 1:1 and closes it past 120pt or on a fast flick, else springs back. - Policy is pure and unit-tested (NowPlayingDismissDrag, 16 tests): a drag is judged once on its first sample, so a horizontal scrub never moves the player and a tracked drag never snaps back mid-gesture. - It's a plain .gesture, so child gestures win: the sliders, every button, The Receiver's volume ladder (recognizes on touch-down). - The cover gets a clear presentation background so the app shows through as the player slides. Offset content does NOT paint the parts of its backgrounds that reach into the safe area (measured: the 62pt top band vanished), so the moving layer ignores the system safe area and carries the same insets as its own safeAreaPadding. - VoiceOver: escape (two-finger Z) closes it. At rest the player is pixel-identical on iPhone 17 Pro / 26.5. 165 tests. Unverified on hardware: drags that start on a slider thumb or a button, and the Duo split under the new safe-area wrapper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VriHZgxfrBNm1js1UNLYTN
Tim reversed "player alone when upright". ArrangementView's horizontal-only split refuses a taller-than-wide canvas, so the pure NowPlayingColumns policy picks the layout: compact → player alone; regular and wider than tall → the verified ArrangementView; regular and upright → an explicit HStack, queue clamp(width − 300, 220…260)pt so the player never gets less than 300pt (sim upright: 409 + 260). Both columns report a compact size class, as ArrangementView's do. Compact still routes through ArrangementView's primary-only path, so a fold keeps the player's state as the fold test verified; switching between the HStack and ArrangementView (rotating with the cover open) rebuilds the player — documented trade-off. Every player now fits a 300pt column (verified with a forced-300pt build): nowPlayingFitWidth(margin:) scales the Standard transport row (~308pt natural) and The Receiver's knurled row (364pt) as units, scale 1 on every iPhone. Also fixes Click Wheel's pad never shrinking below 330pt: a max-only frame reports max(child, proposal), so the container needed minWidth: 0. 8 new tests (173). iPhone 17 Pro / 26.5 byte-identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VriHZgxfrBNm1js1UNLYTN
… real-Duo pass Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VriHZgxfrBNm1js1UNLYTN
- The pre-27.1 path chose between "player alone" and "player + queue" with a switch; each case of a switch in a view builder is its own branch, so crossing it rebuilt the player and dropped its state. One call with an optional queue width now keeps the player's identity. - close() dismisses directly when the arrangement is already off-screen: an animation that changes nothing isn't guaranteed to run its completion, which would leave a clear, touch-blocking cover. - The cover's clear presentation background keeps the TabView live behind the player, so the arrangement is now an accessibility modal and VoiceOver can't wander onto Home behind it. - Tests pin the inclusive 120pt / 300pt thresholds and that only a tracking drag can close (175 tests). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VriHZgxfrBNm1js1UNLYTN
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adapts the iOS app to iPhone Duo (Apple's foldable) and fixes several bugs the work uncovered on every iPhone.
iPhone Duo
NowPlayingArrangementhosts the full-screen player: iOS 27.1ArrangementView(player + Signal/Up Next) when the inner display is flat, explicit columns when it's upright, the player alone everywhere else (every other iPhone, the outer display, Split View halves). The player's queue button is hidden only while the queue is beside it. Verified folding and unfolding with the player open (state kept, button returns)..expanded; the title had collapsed to "Aw…". Now ±15/30 then previous drop out to keep a 90pt title (all five kept on a normal iPhone).Fixes on every iPhone
fullScreenCover, so only the chevron ever closed it. A drag on the player (never the sliders, buttons, or queue column) now follows the finger and closes past a threshold.CI
Moves to the
xcode-27runner, pinned toXcode_27.1.app, with a step that fails loudly if that Xcode is missing — the 27.1 SDK symbols can't compile on 26.6 even behind#available. This PR is the workflow's first run on that runner. The iOS deployment floor stays 26.1.Verification
SWIFT_STRICT_CONCURRENCY=complete;AppleNugsTests175 passing.24A94425): all five themes, flat / upright / folded.🤖 Generated with Claude Code
https://claude.ai/code/session_01VriHZgxfrBNm1js1UNLYTN