Created
August 9, 2020 20:17
-
-
Save ryanashcraft/41af6d9eca21c013d06f07a2cb1c5af1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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