You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI倒计时视图无动画:参数初始化为何导致动画失效?

SwiftUI 环形倒计时动画无响应问题解析

问题描述

我尝试用SwiftUI实现一个环形倒计时视图,代码如下:

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
    }
}

期望实现的环形倒计时动画效果如下:
环形倒计时动画

但当前视图完全没有动画,始终停留在初始位置。奇怪的是,若直接用字面量初始化@State var timeRemaining: Double = 20.0,并将period替换为字面量数值,动画就能正常运行。

问题根源

核心问题在于**@State属性的初始化逻辑与SwiftUI视图更新机制的冲突**:

  1. 静态存储属性timer的初始化时机错误
    你将timer声明为private let timer = Timer.publish(...),这是一个静态存储属性,它会在CountdownView类型首次被访问时就完成初始化,而非在实例创建阶段。这会导致定时器提前启动发送事件,而此时视图尚未完成首次渲染,后续timeRemaining的变更无法触发视图的动画重绘。

  2. 实例属性period的依赖追踪失效
    endAngle的计算依赖实例属性period,当period通过初始化参数传入而非字面量时,SwiftUI无法在初始化阶段正确建立endAngle与period的依赖关系。当timeRemaining变更时,endAngle的计算结果变化无法被SwiftUI感知,因此不会触发环形路径的动画更新。

解决方案

针对上述问题,可通过以下两种方式调整代码:

方式一:延迟初始化定时器

将timer改为延迟初始化的存储属性,确保其在实例创建后才初始化:

struct CountdownView: View {
    
    private let duration: Double
    private let period: Double
    
    @State
    var timeRemaining: Double = 20.0
    
    // 改为延迟初始化
    private lazy var timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
    
    init(duration: Double, period: Double) {
        self.duration = duration
        self.period = period
        _timeRemaining = State(initialValue: duration)
    }
    
    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
                    }
                }
            }
    }
}

方式二:将period转为@State属性

让period成为可被SwiftUI追踪的状态属性,确保endAngle的依赖关系被正确识别:

struct CountdownView: View {
    
    private let duration: Double
    @State private var period: Double
    
    @State
    var timeRemaining: Double = 20.0
    
    private let timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
    
    init(duration: Double, period: Double) {
        self.duration = duration
        _period = State(initialValue: period)
        _timeRemaining = State(initialValue: duration)
    }
    
    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
                    }
                }
            }
    }
}

调整后,timeRemaining的变更会正确触发endAngle的计算,进而带动环形路径的动画更新。

内容的提问来源于stack exchange,提问作者Overpass

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 01:07:05