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.
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)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment