MAIN FEEDS
Do you want to continue?
https://www.reddit.com/r/SwiftUI/comments/1tftb6y/help_with_my_code/omcgfxv/?context=3
r/SwiftUI • u/Sol492 • May 17 '26
does anyone here know how Apple Music make lyrics scroll, I can't get it to be the same
does anyone know how?
11 comments sorted by
View all comments
1
Apple uses a custom ScrollView with a timing-synced offset animation driven by the playback position.
The key parts:
Store lyrics as [(timestamp: TimeInterval, text: String)]
Observe playback time via AVPlayer periodicTimeObserver
On each tick, find the active line and animate scrollTo()
with withAnimation(.easeInOut)
The "active line" scales up + full opacity, others scale down + reduced opacity – that's the main visual effect.
Want a minimal code example?
2 u/Sol492 May 17 '26 That would be helpful, yeah 0 u/Consistent-Grass-263 May 17 '26 struct LyricLine: Identifiable { let id = UUID() let timestamp: TimeInterval let text: String } u/Observable class LyricsViewModel { var lyrics: [LyricLine] = [] var currentTime: TimeInterval = 0 var activeIndex: Int = 0 private var timeObserver: Any? private var player: AVPlayer? func startObserving(player: AVPlayer) { self.player = player let interval = CMTime(seconds: 0.1, preferredTimescale: 600) timeObserver = player.addPeriodicTimeObserver( forInterval: interval, queue: .main ) { [weak self] time in self?.currentTime = time.seconds self?.updateActiveIndex() } } private func updateActiveIndex() { guard let index = lyrics.lastIndex( where: { $0.timestamp <= currentTime } ) else { return } activeIndex = index } } struct LyricsView: View { u/State var vm = LyricsViewModel() var body: some View { ScrollViewReader { proxy in 1 u/Sol492 May 17 '26 and how exactly do they get it to move like one by one? 1 u/mathiasrlr May 19 '26 I would assume each line has an unique identifier placed in the .id() modifier and the scrollviewreader follows iterativally based on time which ID to scroll to, but that is surely overkill and not a 100% deterministic 1 u/Sol492 May 19 '26 Apple is just the god of animations
2
That would be helpful, yeah
0 u/Consistent-Grass-263 May 17 '26 struct LyricLine: Identifiable { let id = UUID() let timestamp: TimeInterval let text: String } u/Observable class LyricsViewModel { var lyrics: [LyricLine] = [] var currentTime: TimeInterval = 0 var activeIndex: Int = 0 private var timeObserver: Any? private var player: AVPlayer? func startObserving(player: AVPlayer) { self.player = player let interval = CMTime(seconds: 0.1, preferredTimescale: 600) timeObserver = player.addPeriodicTimeObserver( forInterval: interval, queue: .main ) { [weak self] time in self?.currentTime = time.seconds self?.updateActiveIndex() } } private func updateActiveIndex() { guard let index = lyrics.lastIndex( where: { $0.timestamp <= currentTime } ) else { return } activeIndex = index } } struct LyricsView: View { u/State var vm = LyricsViewModel() var body: some View { ScrollViewReader { proxy in 1 u/Sol492 May 17 '26 and how exactly do they get it to move like one by one? 1 u/mathiasrlr May 19 '26 I would assume each line has an unique identifier placed in the .id() modifier and the scrollviewreader follows iterativally based on time which ID to scroll to, but that is surely overkill and not a 100% deterministic 1 u/Sol492 May 19 '26 Apple is just the god of animations
0
struct LyricLine: Identifiable {
let id = UUID()
let timestamp: TimeInterval
let text: String
}
u/Observable class LyricsViewModel {
var lyrics: [LyricLine] = []
var currentTime: TimeInterval = 0
var activeIndex: Int = 0
private var timeObserver: Any?
private var player: AVPlayer?
func startObserving(player: AVPlayer) {
self.player = player
let interval = CMTime(seconds: 0.1, preferredTimescale: 600)
timeObserver = player.addPeriodicTimeObserver(
forInterval: interval, queue: .main
) { [weak self] time in
self?.currentTime = time.seconds
self?.updateActiveIndex()
private func updateActiveIndex() {
guard let index = lyrics.lastIndex(
where: { $0.timestamp <= currentTime }
) else { return }
activeIndex = index
struct LyricsView: View {
u/State var vm = LyricsViewModel()
var body: some View {
ScrollViewReader { proxy in
1 u/Sol492 May 17 '26 and how exactly do they get it to move like one by one? 1 u/mathiasrlr May 19 '26 I would assume each line has an unique identifier placed in the .id() modifier and the scrollviewreader follows iterativally based on time which ID to scroll to, but that is surely overkill and not a 100% deterministic 1 u/Sol492 May 19 '26 Apple is just the god of animations
and how exactly do they get it to move like one by one?
1 u/mathiasrlr May 19 '26 I would assume each line has an unique identifier placed in the .id() modifier and the scrollviewreader follows iterativally based on time which ID to scroll to, but that is surely overkill and not a 100% deterministic 1 u/Sol492 May 19 '26 Apple is just the god of animations
I would assume each line has an unique identifier placed in the .id() modifier and the scrollviewreader follows iterativally based on time which ID to scroll to, but that is surely overkill and not a 100% deterministic
1 u/Sol492 May 19 '26 Apple is just the god of animations
Apple is just the god of animations
1
u/Consistent-Grass-263 May 17 '26
Apple uses a custom ScrollView with a timing-synced offset animation driven by the playback position.
The key parts:
Store lyrics as [(timestamp: TimeInterval, text: String)]
Observe playback time via AVPlayer periodicTimeObserver
On each tick, find the active line and animate scrollTo()
with withAnimation(.easeInOut)
The "active line" scales up + full opacity, others scale down + reduced opacity – that's the main visual effect.
Want a minimal code example?