This is my current working solution but it seems wonky. I kinda poked around with the numbers until it worked but I don't fully understand the math. Below is my code. The second circle .trim
is were I think the main problem lies.
struct ContentView: View {
@State private var timeRemaining: TimeInterval = 10
@State private var startTime: Double = 10
@State private var timer: Timer?
@State private var isRunning: Bool = false
var body: some View {
ZStack {
Circle()
.trim(from: 0.1, to: 0.9)
.stroke(style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round))
.rotationEffect(.degrees(90))
.opacity(0.3)
Circle()
.trim(from: 0.1, to: CGFloat((1.1 - (timeRemaining / (startTime) - 0.026)) / 1.25))
.stroke(style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round))
.rotationEffect(.degrees(90))
.animation(.easeInOut(duration: 1))
Text(formattedTime())
.font(.largeTitle)
.bold()
}
.frame(width: 250)
Button {
isRunning.toggle()
if isRunning {
startTimer()
} else {
stopTimer()
}
} label: {
Image(systemName: isRunning ? "stop.fill" : "play.fill")
.foregroundStyle(.foreground)
.frame(width: 50, height: 50)
.font(.largeTitle)
.padding()
}
}
private func startTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
if timeRemaining > 0 {
timeRemaining -= 1
} else {
stopTimer()
}
}
}
private func stopTimer() {
isRunning = false
timer?.invalidate()
timeRemaining = 10
startTime = Double(timeRemaining)
}
private func formattedTime() -> String {
let minutes = Int(timeRemaining) / 60
let second = Int(timeRemaining) % 60
return String(format: "%02d:%02d", minutes, second)
}
}
You could just make the animation run for the same time as the timer is counting down. It needs to be linear
too.
In order for the dot to be visible at the start, the initial position must be anything larger than 0.1. 0.101 works fine.
Circle()
.trim(from: 0.1, to: isRunning ? 0.9 : 0.101)
.stroke(style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round))
.rotationEffect(.degrees(90))
.animation(.linear(duration: isRunning ? timeRemaining : 0.5), value: isRunning)