miniplayer fix

Two fixes:
Snap-back on seek end: Previously isScrubbing = false was set
immediately after seekToPercent(pct). AVPlayer.seek is asynchronous —
currentTime doesn’t update instantly. So displayProgress switched from
scrubPosition back to the old playbackTime / playbackDuration for up
to 250ms until the timer polled again, causing a visible snap-back.
Now scrubPosition is held at pct and isScrubbing clears after a 150ms
delay — enough time for AVPlayer to confirm the seek position before
the bar resumes tracking currentTime.
Stale timer poller removed: playbackTime and playbackDuration were
@State vars updated by a Timer.publish every 0.25s. Since currentTime
and duration are already @Published on AudioPlayer, the timer was just
adding lag and a secondary update cycle. They’re now computed
properties reading directly from audioPlayer.currentTime and
audioPlayer.duration — updates arrive instantly via SwiftUI’s
observation, same as the full Now Playing view.
This commit is contained in:
Dallas Groot 2026-04-10 07:33:14 -07:00
parent 58be99b40b
commit c8360419c3

View file

@ -492,20 +492,19 @@ struct MiniPlayerBar: View {
var namespace: Namespace.ID
@State private var isScrubbing = false
@State private var scrubPosition: Double = 0
@State private var playbackTime: TimeInterval = 0
@State private var playbackDuration: TimeInterval = 0
private let accentPink = Color(red: 1.0, green: 0.176, blue: 0.333)
// Poll currentTime at our own pace doesn't trigger parent view redraws
private let timePoller = Timer.publish(every: 0.25, on: .main, in: .common).autoconnect()
// Read directly from @Published properties no timer lag
private var playbackTime: TimeInterval { audioPlayer.currentTime }
private var playbackDuration: TimeInterval { audioPlayer.duration }
private var displayProgress: Double {
if isScrubbing { return scrubPosition }
guard playbackDuration > 0 else { return 0 }
return min(playbackTime / playbackDuration, 1.0)
}
var body: some View {
VStack(spacing: 0) {
// Scrubbable progress bar at top uses album color, generous touch target
@ -514,14 +513,14 @@ struct MiniPlayerBar: View {
Rectangle()
.fill(Color.white.opacity(0.1))
.frame(height: isScrubbing ? 8 : 3)
Rectangle()
.fill(colorExtractor.isLoaded ? colorExtractor.primaryColor : accentPink)
.frame(
width: geo.size.width * displayProgress,
height: isScrubbing ? 8 : 3
)
if isScrubbing {
Circle()
.fill(colorExtractor.isLoaded ? colorExtractor.primaryColor : accentPink)
@ -540,11 +539,17 @@ struct MiniPlayerBar: View {
.onEnded { value in
let pct = min(max(value.location.x / geo.size.width, 0), 1)
audioPlayer.seekToPercent(pct)
isScrubbing = false
// Hold scrubPosition until AVPlayer confirms the seek
// without this the bar snaps back to the pre-seek position
// for up to 250ms while currentTime catches up.
scrubPosition = pct
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
isScrubbing = false
}
}
)
}
.frame(height: isScrubbing ? 20 : 14) // Generous touch target even when not scrubbing
.frame(height: isScrubbing ? 20 : 14)
.animation(.easeInOut(duration: 0.15), value: isScrubbing)
// Player content
@ -639,10 +644,6 @@ struct MiniPlayerBar: View {
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
.shadow(color: .black.opacity(0.3), radius: 8, y: 4)
.padding(.horizontal, 8)
.onReceive(timePoller) { _ in
playbackTime = audioPlayer.currentTime
playbackDuration = audioPlayer.duration
}
}
}