Last active
March 5, 2025 10:01
-
-
Save Koshimizu-Takehito/f30712ddea1230a58062594b86ad6ef3 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 | |
// https://x.com/TAAT626/status/1895841081365053901 | |
// https://gist.github.com/TAATHub/8f9e7d987c82ef0eea62d2e420d51144 | |
struct CountdownView: View { | |
@State private var counter = Counter() | |
var body: some View { | |
let radius = 120.0 | |
ZStack { | |
Text("\(Int(counter.count + 0.99))") | |
.fontDesign(.rounded) | |
.font(.system(size: 60, weight: .bold)) | |
.monospacedDigit() | |
.contentTransition(.numericText(countsDown: true)) | |
.transaction { $0.animation = counter.count > 1 ? $0.animation : nil } | |
.animation(.default, value: counter.count) | |
ZStack { | |
ForEach(Array(0..<36), id: \.self) { angle in | |
Capsule() | |
.frame(width: 8, height: 24) | |
.offset(x: 0, y: radius - 24.0/2.0) | |
.rotationEffect(.degrees(Double(angle) * 10)) | |
} | |
} | |
.mask { | |
Circle() | |
.trim(from: 0, to: degree) | |
.stroke(lineWidth: 2 * 24) | |
.frame(width: radius * 2, height: radius * 2) | |
.rotationEffect(.degrees(-90.0 - 5.0)) | |
} | |
} | |
.frame(width: 2 * radius, height: 2 * radius) | |
.clipShape(.circle) | |
.contentShape(.circle) | |
.foregroundStyle(counter.count > 0 ? .black : .red) | |
.onTapGesture { | |
Task { await counter.restart() } | |
} | |
} | |
private var degree: Double { | |
1.0 - counter.count.truncatingRemainder(dividingBy: 1.0) | |
} | |
} | |
#Preview { | |
CountdownView() | |
} |
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 Foundation | |
@MainActor | |
@Observable | |
final class Counter { | |
private(set) var count: Double = 10 | |
private(set) var task: Task<Void, Never>? { | |
didSet { oldValue?.cancel() } | |
} | |
func restart() async { | |
count = 10 | |
task = Task { [weak self] in | |
let interval = 1.0 / 360.0 | |
let timer = Timer | |
.publish(every: interval, on: .main, in: .common) | |
.autoconnect() | |
for await _ in timer.values { | |
guard let self, count > 0 else { | |
break | |
} | |
count -= interval | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment