Skip to content

Instantly share code, notes, and snippets.

@ryanashcraft
Created August 9, 2020 20:17
Show Gist options
  • Save ryanashcraft/41af6d9eca21c013d06f07a2cb1c5af1 to your computer and use it in GitHub Desktop.
Save ryanashcraft/41af6d9eca21c013d06f07a2cb1c5af1 to your computer and use it in GitHub Desktop.

Revisions

  1. ryanashcraft created this gist Aug 9, 2020.
    225 changes: 225 additions & 0 deletions LyricsDemoApp.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,225 @@
    import SwiftUI

    // Models

    enum Lyric {
    case line(String)
    case pause(TimeInterval)
    }

    class ScrollToModel: ObservableObject {
    @Published var activeLyricIndex: Int = 0

    var lyrics: [Lyric] = [
    .pause(2),
    .line("Now and then I think of when we were together"),
    .line("Like when you said you felt so happy you could die"),
    .line("Told myself that you were right for me"),
    .line("But felt so lonely in your company"),
    .line("But that was love and it's an ache I still remember"),
    .line("You can get addicted to a certain kind of sadness"),
    .line("Like resignation to the end, always the end"),
    .line("So when we found that we could not make sense"),
    .line("Well, you said that we would still be friends"),
    .line("But I'll admit that I was glad that it was over"),
    .line("But you didn't have to cut me off"),
    .line("Make out like it never happened and that we were nothing"),
    .line("And I don't even need your love"),
    .line("But you treat me like a stranger and that feels so rough"),
    .line("No, you didn't have to stoop so low"),
    .line("Have your friends collect your records and then change your number"),
    .line("I guess that I don't need that though"),
    .line("Now you're just somebody that I used to know"),
    .line("Now you're just somebody that I used to know"),
    .line("Now you're just somebody that I used to know"),
    .line("Now and then I think of all the times you screwed me over"),
    .line("But had me believing it was always something that I'd done"),
    .line("But I don't wanna live that way"),
    .line("Reading into every word you say"),
    .line("You said that you could let it go"),
    .line("And I wouldn't catch you hung up on somebody that you used to know"),
    .line("But you didn't have to cut me off"),
    .line("Make out like it never happened and that we were nothing"),
    .line("And I don't even need your love"),
    .line("But you treat me like a stranger and that feels so rough"),
    .line("No, you didn't have to stoop so low"),
    .line("Have your friends collect your records and then change your number"),
    .line("I guess that I don't need that though"),
    .line("Now you're just somebody that I used to know"),
    .line("Somebody (I used to know)"),
    .line("Somebody (Now you're just somebody that I used to know)"),
    .line("Somebody (I used to know)"),
    .line("Somebody (Now you're just somebody that I used to know)"),
    .line("I used to know"),
    .line("That I used to know"),
    .line("I used to know"),
    .line("Somebody"),
    ]

    func lyric(for index: Int) -> Lyric? {
    if index >= 0 && index < lyrics.count {
    return lyrics[index]
    }

    return nil
    }

    func lyricDuration(for lyric: Lyric) -> TimeInterval {
    switch lyric {
    case .line(let text):
    let wordCount = text.split(separator: " ").count
    var duration = TimeInterval(wordCount) * 0.4

    if wordCount < 5 {
    duration += TimeInterval.random(in: 0...8)
    }

    return duration
    case .pause(let pauseDuration):
    return pauseDuration
    }
    }

    func scheduleNextLyric() {
    guard let currentLyric = lyric(for: activeLyricIndex) else {
    return
    }

    let duration = lyricDuration(for: currentLyric)

    let timer = Timer(timeInterval: duration, repeats: false) { _ in
    self.activeLyricIndex += 1

    self.scheduleNextLyric()
    }

    RunLoop.main.add(timer, forMode: .default)
    }

    func play() {
    scheduleNextLyric()
    }
    }

    // Main content

    struct ContentView: View {
    @StateObject var model = ScrollToModel()

    var body: some View {
    ScrollView {
    ScrollViewReader { scrollProxy in
    LazyVStack(alignment: .leading, spacing: 0) {
    ForEach(Array(model.lyrics.enumerated()), id: \.0) { i, lyric in
    Group {
    switch lyric {
    case .line(let lyricText):
    Text(lyricText)
    .multilineTextAlignment(.leading)
    .font(Font.title.bold())
    .frame(maxWidth: .infinity, alignment: .leading)
    case .pause:
    EllipsisSpinner(size: .large)
    .scaleEffect(model.activeLyricIndex == i ? 1 : 0.5, anchor: .center)
    .opacity(model.activeLyricIndex == i ? 1 : 0)
    .animation(Animation.easeIn(duration: 0.4))
    }
    }
    .foregroundColor(Color(hue: 0.12, saturation: 0.32, brightness: 0.93))
    .opacity(model.activeLyricIndex == i ? 1 : 0.5)
    .blur(radius: model.activeLyricIndex == i ? 0 : 2)
    .animation(.default)
    .padding()
    }
    }.onReceive(model.$activeLyricIndex) { activeLyricIndex in
    withAnimation {
    scrollProxy.scrollTo(activeLyricIndex, anchor: .center)
    }
    }
    }
    }
    .background(
    Image("gotye")
    .resizable()
    .scaledToFill()
    .scaleEffect(6, anchor: .center)
    .transformEffect(CGAffineTransform(translationX: -100, y: -100))
    .blur(radius: 30)
    .blendMode(.multiply)
    .background(
    Color(hue: 0.07, saturation: 0.70, brightness: 0.71)
    )
    .edgesIgnoringSafeArea(.all)
    )
    .onAppear {
    model.play()
    }
    }
    }

    // Ellipsis spinner

    struct EllipsisSpinner: View {
    enum Size {
    case small
    case large

    var circleSize: CGFloat {
    switch self {
    case .small:
    return 4
    case .large:
    return 14
    }
    }

    var spacing: CGFloat {
    switch self {
    case .small:
    return 3
    case .large:
    return 6
    }
    }
    }

    let size: Size

    @State private var counter: Int = 0
    @State private var isScaled: Bool = false
    @State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

    var body: some View {
    HStack(alignment: .center, spacing: self.size.spacing) {
    ForEach(0 ... 2, id: \.self) { i in
    Circle()
    .frame(
    width: self.size.circleSize,
    height: self.size.circleSize
    )
    .opacity(self.counter % 3 == i ? 1.0 : 0.3)
    .animation(.default)
    }
    }
    .scaleEffect(isScaled ? 1.1 : 1, anchor: .center)
    .animation(Animation.easeInOut(duration: 3).repeatForever(autoreverses: true))
    .onReceive(timer) { _ in
    self.counter += 1
    }
    .onAppear {
    self.isScaled.toggle()
    }
    }
    }

    // App

    @main
    struct LyricsDemoApp: App {
    var body: some Scene {
    WindowGroup {
    ContentView()
    .preferredColorScheme(.dark)
    }
    }
    }