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:
parent
58be99b40b
commit
c8360419c3
1 changed files with 17 additions and 16 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue