SwiftUI Canvas ring animation briefly rotates backward after app returns from background

Hi,

I have a SwiftUI "work time" screen with a rotating ring (60 tick marks, Canvas-based). While the app stays in foreground, rotation is fine.

After the app is in background for a while and comes back to foreground, I consistently see one visual glitch:

  • the ring makes one very short step in the opposite direction once
  • then continues rotating clockwise normally

So this is not a crash, only a visual reverse tick on resume.

What I expect:

  • no direction change after foreground resume
  • continuous clockwise motion

What I already tried:

  1. withAnimation(.linear(...).repeatForever(...)) + restart on scenePhase
  2. TimelineView (.animation and .periodic) with time-based angle
  3. angle with and without modulo wrapping
  4. wall-clock and monotonic time sources
  5. rotation via rotationEffect and also via Canvas geometry
  6. warmup delays after resume
  7. restoring original ring visuals (long/short tick marks)

The effect is still reproducible.

Question: What is the correct SwiftUI approach to implement a continuously rotating ring that stays direction-stable across background/foreground transitions, with no one-frame reverse step on resume?

Any pattern that is robust on current iOS versions and avoids visual artifacts on scene phase changes would be appreciated.

Minimal repro:

import SwiftUI

struct ReproClockView: View {
    @Environment(\.scenePhase) private var scenePhase
    private let tickCount = 60
    private let rotationDuration: Double = 120

    @State private var rotationDegrees: Double = 0
    @State private var hasAppeared = false

    var body: some View {
        ZStack {
            Canvas { context, size in
                let center = CGPoint(x: size.width / 2, y: size.height / 2)
                let radius = min(size.width, size.height) / 2 - 8

                for index in 0..<tickCount {
                    let angle = Double(index) * (360.0 / Double(tickCount)) - 90
                    let radians = angle * .pi / 180
                    let isLongTick = index % 5 == 0
                    let length: CGFloat = isLongTick ? 22 : 14

                    let outerRadius = radius
                    let innerRadius = radius - length

                    let startPoint = CGPoint(
                        x: center.x + cos(radians) * outerRadius,
                        y: center.y + sin(radians) * outerRadius
                    )
                    let endPoint = CGPoint(
                        x: center.x + cos(radians) * innerRadius,
                        y: center.y + sin(radians) * innerRadius
                    )

                    var path = Path()
                    path.move(to: startPoint)
                    path.addLine(to: endPoint)

                    context.stroke(
                        path,
                        with: .color(.red),
                        style: StrokeStyle(lineWidth: 2.5, lineCap: .round)
                    )
                }
            }
            .rotationEffect(.degrees(rotationDegrees))
            .drawingGroup()
        }
        .frame(width: 340, height: 340)
        .onAppear {
            guard !hasAppeared else { return }
            hasAppeared = true
            startRotation()
        }
        .onChange(of: scenePhase) { oldPhase, newPhase in
            if oldPhase == .background && newPhase == .active {
                withAnimation(.linear(duration: 0)) {
                    rotationDegrees = 0
                }
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
                    startRotation()
                }
            }
        }
    }

    private func startRotation() {
        rotationDegrees = 0
        withAnimation(.linear(duration: rotationDuration).repeatForever(autoreverses: false)) {
            rotationDegrees = 360
        }
    }
}
SwiftUI Canvas ring animation briefly rotates backward after app returns from background
 
 
Q