Skip to content

Instantly share code, notes, and snippets.

@ordinaryindustries
Created February 27, 2025 16:34
Show Gist options
  • Select an option

  • Save ordinaryindustries/a27bffeee246c17635668136eb536e51 to your computer and use it in GitHub Desktop.

Select an option

Save ordinaryindustries/a27bffeee246c17635668136eb536e51 to your computer and use it in GitHub Desktop.
import Combine
import SwiftUI
struct CountdownView: View {
@State private var countdown: Int = 15 * 60
@State private var displayedTime = "15:00"
@State private var countdownTimer: AnyCancellable?
@State private var isShowingLaurels = false
@State private var isShowingExpiry = false
@State private var isShowingCountdown = false
var body: some View {
HStack {
LaurelIcon(isShowing: isShowingLaurels, direction: .leading)
VStack {
Text("Expires in")
.bold()
.opacity(0.6)
.opacity(isShowingExpiry ? 0.6 : 0)
.offset(y: isShowingExpiry ? 0 : 50)
.animation(.spring(response: 0.4, dampingFraction: 0.4, blendDuration: 1.0), value: isShowingExpiry)
Text(displayedTime)
.fontDesign(.rounded)
.font(.system(size: 60))
.bold()
.monospacedDigit()
.contentTransition(.numericText(countsDown: true))
.opacity(isShowingCountdown ? 1 : 0)
.blur(radius: isShowingCountdown ? 0 : 10)
.scaleEffect(isShowingCountdown ? 1 : 1.2)
.animation(.spring(response: 0.4, dampingFraction: 0.4, blendDuration: 1.0), value: isShowingCountdown)
}
LaurelIcon(isShowing: isShowingLaurels, direction: .trailing)
}
.onAppear {
animateEntrance()
startCountdown()
}
.onDisappear {
resetAnimations()
stopCountdown()
}
.padding()
}
}
extension CountdownView {
private func animateEntrance() {
Task {
isShowingCountdown = false
isShowingLaurels = false
isShowingExpiry = false
try? await Task.sleep(nanoseconds: 100_000_000)
withAnimation { isShowingCountdown = true }
try? await Task.sleep(nanoseconds: 100_000_000)
withAnimation { isShowingLaurels = true }
try? await Task.sleep(nanoseconds: 100_000_000)
withAnimation { isShowingExpiry = true }
}
}
private func resetAnimations() {
isShowingCountdown = false
isShowingLaurels = false
isShowingExpiry = false
}
private func startCountdown() {
stopCountdown()
countdownTimer = Timer.publish(every: 1, on: .main, in: .common)
.autoconnect()
.sink { _ in
if countdown > 0 {
countdown -= 1
updateDisplayedTime()
} else {
stopCountdown()
}
}
}
private func stopCountdown() {
countdownTimer?.cancel()
countdownTimer = nil
}
private func updateDisplayedTime() {
let minutes = countdown / 60
let seconds = countdown % 60
let newTime = String(format: "%02d:%02d", minutes, seconds)
withAnimation {
displayedTime = newTime
}
}
}
struct LaurelIcon: View {
var isShowing: Bool
var direction: Direction
enum Direction {
case leading, trailing
}
var body: some View {
Image(systemName: direction == .leading ? "laurel.leading" : "laurel.trailing")
.font(.system(size: 50))
.opacity(isShowing ? 0.6 : 0)
.offset(x: isShowing ? 0 : (direction == .leading ? 50 : -50))
.animation(.spring(response: 0.4, dampingFraction: 0.5), value: isShowing)
}
}
#Preview("Countdown") {
CountdownView()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment