编辑2:我在下面添加了一个屏幕录制,这样问题就更清楚了.我对当前的答案并不满意,因为我不明白为什么绑定(binding)在处理数组时会有不同的更新,而在处理单个 struct (即使它们是相同类型的)时会更可靠.

Demo of suspected concurrency issue

-

编辑:我更新了代码(删除了ForEach并添加了几个print()),使缺少线程安全数组的问题更加明显.

-

我在iOS上编写了一个基本的倒计时计时器,在更新@ State变量之前使用try await Task.sleep(for: .seconds(1)).当变量MyStruct时,它似乎更新得很好,但如果我在数组中放置几个MyStruct—s,那么我会得到奇怪的更新时间, destruct 了基本计时器的功能.

这是我的孤立示例--您可能必须使用refresh the app a few times或用一个按钮替换我的onOuar,才能看到这个行为有多不一致.

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上同样奇怪的结果

推荐答案

虽然这段代码中有很多问题,但根本问题是您有多个线程同时更新同一数组中的单个值类型实例.

具体地说,startTimer是一个非隔离的async函数,它(由于SE-0338)不在主线程上运行.因此,您有多个线程更新同一数组中的单个值,而没有足够的同步.很难具体地描述所显示的行为(因为它依赖于很多Foundation实现细节),但是同时从不同线程在同一个数组中变异多个值也就不足为奇了.

FWIW,将startTimer个孤立给一个全球参与者解决了眼前的问题.在这个简单的例子中,当更新View使用的属性时,我会将它与主参与者隔离.

但这并不是全部答案:至少,我建议将"严格并发判断"构建设置更改为"完成",并判断/解决它引起的所有警告.

Ios相关问答推荐

缺少预期的键:';NSPrival yCollectedDataTypes';

AVFoundation Camera推出变焦SWIFT

有没有办法将滚动视图RTL(阿拉伯语)与Ltr动画一起使用?

当目标位于菜单标签内时,matchedGeometryEffect 的行为很奇怪

SwiftUI 中的描边图像边框

我需要二进制文件来进行 App Store 应用程序传输吗?

在 Swift 中映射 MySQL 数据

-1103 错误域=NSURLErrorDomain 代码=-1103资源超过最大大小iOS 13

实现一个将块用作回调的方法

如何关闭情节提要弹出框

iOS:使用 UIView 的 'drawRect:' 与其层的委托 'drawLayer:inContext:'

判断可选数组是否为空

如何覆盖@synthesized getter?

如何在 iOS 中判断暗模式?

try 在 iOS 中处理后退导航按钮操作

在 UITableViewController 中使用 UISearchDisplayController 时断言失败

UICollectionView:必须用非零布局参数初始化

具有两种不同字体大小的 NSAttributedString 示例?

在 iOS 7 中更改返回按钮会禁用滑动导航返回

dismissModalViewController 并传回数据