Created
February 27, 2025 16:34
-
-
Save ordinaryindustries/a27bffeee246c17635668136eb536e51 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 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