0

I'm trying to implement a countdown view in SwiftUI as follows:

struct CountdownView: View {
    
    private let duration: Double
    private let period: Double
    
    @State
    var timeRemaining: Double = 20.0
    
    init(duration: Double, period: Double) {
        self.duration = duration
        self.period = period
        _timeRemaining = State(initialValue: duration)
    }
    
    private let timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
    
    private var endAngle: Double { 360.0 * (1 - timeRemaining / period) - 90.0 }
    
    var body: some View {
        Arc(startAngle: .degrees(270), endAngle: .degrees(endAngle), clockwise: true)
            .foregroundColor(Color.blue)
            .animation(.linear(duration: 0.1))
            .frame(width: 50, height: 50)
            .onReceive(timer) { _ in
                if self.timeRemaining > 0 {
                    withAnimation {
                        self.timeRemaining -= 0.1
                    }
                }
            }
    }
}

struct Arc: Shape {
    
    let startAngle: Angle
    let endAngle: Angle
    let clockwise: Bool
    
    func path(in rect: CGRect) -> Path {
        var path = Path()
        let radius = max(rect.size.width, rect.size.height) / 2
        let centerPoint = CGPoint(x: rect.midX, y: rect.midY)
        path.move(to: centerPoint)
        path.addArc(center: centerPoint,
                    radius: radius,
                    startAngle: startAngle,
                    endAngle: endAngle,
                    clockwise: clockwise
        )
        return path
    }
}

What I expect is something like this:

countdown timer image

However, the View doesn't animate at all, it's just stuck in the initial position indefinitely. But if I initialize the state with a literal at once like this @State var timeRemaining: Double = 20.0 and replace period with some literal number, then the animation works.

What might be the cause of this?

Overpass
  • 433
  • 5
  • 12
  • 3
    Each time a SwiftUI view is redrawn a new struct instance is created. This means that you keep reinitialising `timeRemaining` with the initial value. Decrementing a counter isn't a good way of tracking time because `Timer` isn't particularly accurate and has a lot of jitter. You should compute a `Date` at which your timer ends. Take a look at https://medium.com/@pwilko/how-not-to-create-stopwatch-in-swift-e0b7ff98880f – Paulw11 Apr 09 '23 at 20:56

1 Answers1

0

You have to let SwiftUI know you want to animate a variable in a struct. Add this to struct ARC...

    var animatableData: Angle {
        get { endAngle }
        set { endAngle = newValue }
    }

Also, change endAngle from a let to a var.

P. Stern
  • 279
  • 1
  • 5