Phase 6: lock-screen now-playing widget#6
Conversation
Adds state L1 (PRD §3.2, reference image 1): a live now-playing widget on the lock-screen backdrop. LockScreenNowPlayingWidget is a frosted card (album art, title/artist, scrubber, prev/pause-play/next) floating in the lower-middle over the wallpaper, wired to the shared MediaMetadataService — same live Spotify state and transport controls the notch uses. Reuses ScrubberView from the notch player. Shown for any active track (playing or paused), fades out to just wallpaper + clock when idle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request integrates the new LockScreenNowPlayingWidget into the pseudo lock screen backdrop (LockScreenBackdropView). It updates AppDelegate and PseudoLockScreenController to pass the MediaMetadataService dependency down to the backdrop view, enabling the widget to dynamically display active track metadata and handle playback controls. Feedback was provided on the use of .position(x:y:) in SwiftUI, which can expand the views' layout bounds and interfere with background hit-testing; using .offset(y:) is recommended instead.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| ClockView() | ||
| .position(x: geo.size.width / 2, y: max(96, geo.size.height * 0.17)) | ||
|
|
||
| if case .active(let nowPlaying) = metadata.state { | ||
| LockScreenNowPlayingWidget( | ||
| nowPlaying: nowPlaying, | ||
| albumArt: metadata.albumArt, | ||
| onPlayPause: { Task { await metadata.togglePlayPause() } }, | ||
| onPrevious: { Task { await metadata.skipToPrevious() } }, | ||
| onNext: { Task { await metadata.skipToNext() } }, | ||
| onSeek: { position in Task { await metadata.seek(to: position) } } | ||
| ) | ||
| .position(x: geo.size.width / 2, y: geo.size.height * 0.60) | ||
| .transition(.opacity) | ||
| } |
There was a problem hiding this comment.
Using .position(x:y:) in SwiftUI causes the modified views to expand and occupy the entire available space of their parent container (the full screen inside GeometryReader). This can interfere with hit-testing, potentially blocking user interactions (such as 'click-away' dismissal) on the background WallpaperView even in areas that appear empty.
Using .offset(y:) with a top-aligned ZStack is a safer and more idiomatic alternative because it shifts the views' rendering without expanding their layout bounds to fill the screen.
| ClockView() | |
| .position(x: geo.size.width / 2, y: max(96, geo.size.height * 0.17)) | |
| if case .active(let nowPlaying) = metadata.state { | |
| LockScreenNowPlayingWidget( | |
| nowPlaying: nowPlaying, | |
| albumArt: metadata.albumArt, | |
| onPlayPause: { Task { await metadata.togglePlayPause() } }, | |
| onPrevious: { Task { await metadata.skipToPrevious() } }, | |
| onNext: { Task { await metadata.skipToNext() } }, | |
| onSeek: { position in Task { await metadata.seek(to: position) } } | |
| ) | |
| .position(x: geo.size.width / 2, y: geo.size.height * 0.60) | |
| .transition(.opacity) | |
| } | |
| ClockView() | |
| .offset(y: max(96, geo.size.height * 0.17)) | |
| if case .active(let nowPlaying) = metadata.state { | |
| LockScreenNowPlayingWidget( | |
| nowPlaying: nowPlaying, | |
| albumArt: metadata.albumArt, | |
| onPlayPause: { Task { await metadata.togglePlayPause() } }, | |
| onPrevious: { Task { await metadata.skipToPrevious() } }, | |
| onNext: { Task { await metadata.skipToNext() } }, | |
| onSeek: { position in Task { await metadata.seek(to: position) } } | |
| ) | |
| .offset(y: geo.size.height * 0.60) | |
| .transition(.opacity) | |
| } |
What changed
Phase 6 of the dual-surface build: the live now-playing widget on the lock screen (PRD §3.2 state L1, reference image 1).
LockScreenNowPlayingWidget— a frosted card (album art, title/artist, scrubber, prev/pause-play/next) floating in the lower-middle over the wallpaper. Frosted.ultraThinMaterial+ a dark tint so white text stays legible on any wallpaper.MediaMetadataService— the exact same live Spotify state and transport controls the notch uses; no new plumbing.ScrubberViewfrom the notch player, so the progress bar and drag-to-seek behave identically across surfaces.Why
Continues Surface B toward the reference flow. L1 (this) → L2 click-to-expand (Phase 7) → L3 swipe-to-lyrics (Phase 8).
Base
Stacked on
phase-5-lockscreen-shell(open PR #5, verified but unmerged). This PR's diff is Phase 6 only. Merge #5 first, or rebase ontomain.Notes for review — needs your eyes in Xcode
🤖 Generated with Claude Code