编辑:我更新了代码(删除了 ForEach 并添加了几个 print()),以使缺乏线程安全数组的问题更加明显。
--
在更新 @State 变量之前,我正在使用
try await Task.sleep(for: .seconds(1))
在 iOS 上编写一个基本的倒计时器。当变量 MyStruct
时,它似乎更新得很好,但是如果我在数组中放置一些 MyStruct
-s,那么我会得到奇怪的更新时间,从而破坏了基本计时器倒计时的功能。
这是我的孤立示例 - 您可能需要 刷新应用程序几次或用 Button 替换我的 onAppear 才能查看此行为有多不一致。
import SwiftUI
struct ContentView: View {
@State var first = MyStruct(setTime: 15)
@State var second = MyStruct(setTime: 10)
@State var third = MyStruct(setTime: 5)
@State var array = [MyStruct(setTime: 15), MyStruct(setTime: 10), MyStruct(setTime: 5)]
@State var hasNotAppearedYet = true
var body: some View {
VStack {
Text("Seconds: \(Int(array[0].currentTime))").padding().font(.title3)
Text("Seconds: \(Int(array[1].currentTime))").padding().font(.title3)
Text("Seconds: \(Int(array[2].currentTime))").padding().font(.title3)
Divider()
Text("Seconds: \(first.currentTime)").padding().font(.title3)
Text("Seconds: \(second.currentTime)").padding().font(.title3)
Text("Seconds: \(third.currentTime)").padding().font(.title3)
}
.padding()
.onAppear(){
if(hasNotAppearedYet){
$array[0].startTimer(name: "arrayElement0")
$array[1].startTimer(name: "arrayElement1")
$array[2].startTimer(name: "arrayElement2")
$first.startTimer(name: "FIRST")
$second.startTimer(name: "SECOND")
$third.startTimer(name: "THIRD")
hasNotAppearedYet = false
}
}
}
}
struct MyStruct {
var setTime: Double
var currentTime: Double = 0
}
extension Binding<MyStruct> {
func startTimer(name: String){
Task {
wrappedValue.currentTime = wrappedValue.setTime
print(name, wrappedValue.currentTime)
while (wrappedValue.currentTime > 0) {
try await Task.sleep(for: .seconds(1))
try Task.checkCancellation()
wrappedValue.currentTime -= 1
print(name, wrappedValue.currentTime)
}
}
}
}
#Preview {
ContentView()
}
模拟器和真实 iPhone 上有同样奇怪的结果
您的根本问题是为您的模型使用
struct
而不是 class
。 SwiftUI 要求模型对象符合 @Observable
(或 @ObservableObject
)。您已尝试使用复杂的绑定扩展来使您的结构正常工作,但如果您使用类,那就简单得多。
除了切换到一个类之外,我建议使用结束
Date
而不是递减计数器。 iOS Timer
(及相关构造)在指定的截止日期后的某个时间触发,但不能保证它们会在截止日期或以精确的时间间隔触发。这种变化(称为抖动)可能会导致倒计时值看起来停滞不前,有时会跳动两秒。
通过计算到结束日期所用的时间并以更高的速率运行计时器可以消除这种抖动。
最后,我不确定您的意图是否是尝试 Swift 异步/等待并发,或者这是否只是您提出的解决方案,但此任务不太适合 Swift 异步/等待并发。只需使用
Timer
就简单多了
struct ContentView: View {
@State var first = CountdownTimer(duration: 15)
@State var second = CountdownTimer(duration: 10)
@State var third = CountdownTimer(duration: 5)
@State var array = [CountdownTimer(duration: 15), CountdownTimer(duration: 10), CountdownTimer(duration: 5)]
var body: some View {
VStack {
Text("Seconds: \(Int(array[0].timeRemainingSeconds))").padding().font(.title3)
Text("Seconds: \(Int(array[1].timeRemainingSeconds))").padding().font(.title3)
Text("Seconds: \(Int(array[2].timeRemainingSeconds))").padding().font(.title3)
Divider()
Text("Seconds: \(first.timeRemainingSeconds)").padding().font(.title3)
Text("Seconds: \(second.timeRemainingSeconds)").padding().font(.title3)
Text("Seconds: \(third.timeRemainingSeconds)").padding().font(.title3)
}
.padding()
}
}
@Observable class CountdownTimer {
var timeRemainingSeconds: Int
var timer: Timer?
let endTime: Date
init(duration: Double) {
endTime = Date().addingTimeInterval(duration)
timeRemainingSeconds = Int(duration)
self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { timer in
let remaining = max( self.endTime.timeIntervalSinceNow,0)
if remaining<=0 {
timer.invalidate()
self.timer = nil
}
self.timeRemainingSeconds = Int(remaining)
})
}
}