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

