我在下面添加了屏幕录制,因此问题更加清晰。我对当前的答案不满意,因为我不明白为什么绑定(异步)在处理数组时更新不同,而在处理单个结构时更新更可靠(即使它们是相同类型)
--
在更新 @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 上的奇怪结果相同。
我认为你所看到的是
async await
的自然行为,“演员”决定做什么以及何时做。
在本例中,演员需要兼顾计时器和 UI 更新。
您可以采取一些措施来稳定
while
去掉
Binding
extension
,因为 Binding
不是 Sendable
,因此在 async/await
内使用不安全。您可以打开严格并发来查看警告。 Binding
本身就会引入数据竞争。
使用
.task
代替Task
,这样你就可以抓住并稳定“计时器”。
设置
task
的优先级,它不会是完美的第二个,但应该会好得多。 await
是一秒,但还有其他代码行,因此真实时间可能是 1 秒以上。
import SwiftUI
struct TimerTestView: 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
@State private var date: Date = Calendar.current.date(byAdding: .second, value: 16, to: .init())!
var body: some View {
VStack {
Text(date, style: .timer) //Added an Apple timer to compare
HStack { //Changed UI so I can see things side by side
MyStructView(item: $array[0])
MyStructView(item: $first)
}
HStack {
MyStructView(item: $array[1])
MyStructView(item: $second)
}
HStack {
MyStructView(item: $array[2])
MyStructView(item: $third)
}
}
.padding()
}
}
struct MyStruct: Identifiable, Sendable {
let id: UUID = .init()
var setTime: Double
var currentTime: Double = 0
}
#Preview {
TimerTestView()
}
@MainActor
struct MyStructView: View {
@Binding var item: MyStruct
var body: some View {
Text("Seconds:\n \(item.currentTime)").padding().font(.title3)
// Use the id to stabilize the task
.task(id: item.id, priority: .userInitiated) { //Set the priority & hold on to the Task
do {
try await startTimer(name: item.id.uuidString)
} catch {
print(error)
}
}
}
func startTimer(name: String) async throws {
let start = Date()
item.currentTime = item.setTime
print(name, item.currentTime)
while (item.currentTime > 0) {
try await Task.sleep(for: .seconds(1))
try Task.checkCancellation()
item.currentTime -= 1
print(name, item.currentTime)
}
}
}
虽然此代码中存在很多问题,但根本问题是您有多个线程同时更新同一数组中的各个值类型实例。
具体来说,
startTimer
是一个非隔离的async
函数,它(凭借SE-0338)不在主线程上运行。因此,您有多个线程更新同一数组中的各个值,但没有足够的同步。很难具体说明所表现的行为(因为它依赖于许多 Foundation 实现细节),但同时从不同线程改变同一数组中的多个值是有问题的,这一点也不奇怪。
FWIW,将
startTimer
隔离到全局参与者可以解决眼前的问题。在这个简单的示例中,当您更新 View
使用的属性时,我会将其与主要参与者隔离。
但这不是全部答案:至少,我建议将“严格并发检查”构建设置更改为“完成”,并检查/解决它提出的所有警告。