diff --git a/DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift b/DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift index fcc7afc..0c8f7f2 100644 --- a/DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift +++ b/DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift @@ -365,7 +365,7 @@ struct LockScreenMusicPanel: View { private var progressBar: some View { TimelineView( .animation( - minimumInterval: 0.1, + minimumInterval: 1.0, paused: isProgressTimelinePaused ) ) { timeline in diff --git a/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift b/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift index b62b94b..664dc09 100644 --- a/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift +++ b/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift @@ -654,10 +654,16 @@ private struct MinimalisticReminderDetailsView: View { @State private var dragging: Bool = false @State private var lastDragged: Date = .distantPast + /// Whether the progress timeline should be paused (no ticks). + private var isProgressTimelinePaused: Bool { + !musicManager.isPlaying || musicManager.isLiveStream || musicManager.playbackRate <= 0 + } + private var progressBar: some View { TimelineView( .animation( - minimumInterval: (!musicManager.isLiveStream && musicManager.playbackRate > 0) ? 0.1 : nil + minimumInterval: 1.0, + paused: isProgressTimelinePaused ) ) { timeline in MusicSliderView( diff --git a/DynamicIsland/components/Notch/NotchHomeView.swift b/DynamicIsland/components/Notch/NotchHomeView.swift index 1fe1925..f8603a6 100644 --- a/DynamicIsland/components/Notch/NotchHomeView.swift +++ b/DynamicIsland/components/Notch/NotchHomeView.swift @@ -215,8 +215,13 @@ struct MusicControlsView: View { } } + /// Whether the progress timeline should be paused (no ticks). + private var isProgressTimelinePaused: Bool { + !musicManager.isPlaying || musicManager.isLiveStream || musicManager.playbackRate <= 0 + } + private var musicSlider: some View { - TimelineView(.animation(minimumInterval: musicManager.playbackRate > 0 ? 0.1 : nil)) { timeline in + TimelineView(.animation(minimumInterval: 1.0, paused: isProgressTimelinePaused)) { timeline in MusicSliderView( sliderValue: $sliderValue, duration: $musicManager.songDuration, @@ -639,6 +644,13 @@ struct MusicSliderView: View { guard !dragging, timestampDate.timeIntervalSince(lastDragged) > -1 else { return } sliderValue = MusicManager.shared.estimatedPlaybackPosition(at: newDate) } + .onChange(of: isPlaying) { _, playing in + // Snap slider to the exact position when music pauses so + // the in-flight animation doesn't coast past the true value. + if !playing { + sliderValue = MusicManager.shared.estimatedPlaybackPosition() + } + } .onChange(of: isLiveStream) { isLive in if isLive { sliderValue = 0 @@ -711,6 +723,15 @@ struct MusicSliderView: View { restingTrackHeight: restingTrackHeight, draggingTrackHeight: draggingTrackHeight ) + // Smoothly interpolate the filled track between 1-second ticks using + // Core Animation — runs on the GPU with zero CPU polling cost. + // Disabled while dragging or paused so the bar responds instantly. + .animation( + !dragging && isPlaying && !isLiveStream + ? .linear(duration: 1.0) + : nil, + value: sliderValue + ) } private var sliderTint: Color {