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

Swift 6.0并发Sendable错误排查及解决方案咨询

解决Swift 6.0下Sendable相关错误与警告的思路

问题背景

我开发的应用启动时会读取currency exchange rates,数据存入moneyRate数组,仅部分汇率会被复制到全应用使用的rateArray中。升级到Swift 6.0后出现以下问题:

GetRatesModel中的错误

在GetRatesModel中写入日期、基准货币及汇率到moneyRates时,触发两个错误:

  • Capture of 'self' with non-sendable type 'GetRatesModel' in a 'Sendable Closure'
  • Implicit capture of 'self' requires that GetRatesModel conforms to 'Sendable'

@main文件中的警告

给ViewModel的GetRatesModel添加@preconcurrency宏后,在@main启动文件中出现警告:Sending 'self.vm' risks causing data races(将主线程隔离的'self.vm'发送至非隔离调用方可能引发数据竞争)。

尝试过的方案

  • 将ViewModel放入actor中:除发布日期显示问题外,警告与错误均消失,但根据Hacking with Swift的内容,这并非推荐方案。
  • 尝试用actor作为调用方与被调用方的中介传递本国货币至checkForUpdates,未成功。

疑问

编译器似乎在抱怨checkForUpdates为非隔离方法,若在ContentView的onAppear(置于task内)中调用汇率ViewModel是否有帮助?恳请提供解决这些Sendable错误的思路。


相关代码

@main文件代码

@main
struct MyApp: App {
    
    @State private var vm = GetRatesModel()
    
    @Environment(\.scenePhase) private var scenePhase
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .task(id: scenePhase) {
                    
                    switch scenePhase {
                    case .active:
                        // get today's exchange rates
                        await vm.checkForUpdates(baseCur: base.baseCur.baseS) <- error here
                    case .background:
                    case .inactive:
                    @unknown default: break
                    }
                }
        }
        .environment(vm)
    }
}

ViewModel代码

@Observable final class GetRatesModel {
    
    @ObservationIgnored var currencyCode: String = ""
    @ObservationIgnored var rate: Double = 0.0
    @ObservationIgnored var storedDate: String?
    var lastRateUpdate: Date = Date()
    
    private let monitor = NWPathMonitor()
    var rateArray: [Double] = Array(repeating: 0.0, count: 220)
    var moneyRates = GetRates(date: "2020-07-04", base: "usd", rates: [:])
    
    init() {
            // read in rateArray if available
    }
    
    /*-----------------------------------------------------
     Get exchange rates from network
     Exchange rates are read into moneyRates. Valid country
     exchange rate are then copied to rateArray
     -----------------------------------------------------*/
    func checkForUpdates(baseCur: String) async -> (Bool) {
        
        // don't keep trying to retrieve data when there is no wifi signal
        if await monitor.isConnected() {
            
            // format today's date
            let date = Date.now
            let todayDate = date.formatted(.iso8601.year().month().day().dateSeparator(.dash))

            // read rate change date
            let storedDate = UserDefaults.standard.string(forKey: StorageKeys.upd.rawValue)

            // do currency rate update if storedDate is nil or today's date != storedDate
            if storedDate?.count == nil || todayDate != storedDate {
                
                let rand = Int.random(in: 1000..<10000)
                let sRand = String(rand)
                let requestType = ".json?rand=" + sRand
                let baseUrl = "https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/"

                guard let url = URL(string: baseUrl + baseCur + requestType) else {
                    print("Invalid URL")
                    return self.gotNewRates
                }
                let request = URLRequest(url: url)
                URLSession.shared.dataTask(with: request) { [self] data, response, error in

                    if let data = data {
                        do {
                            // result contains date, base, and rates
                            let result = try JSONSerialization.jsonObject(with: data) as! [String:Any]

                            // store downloaded exchange rates: date, base currency, and rates in moneyRate
                            var keys = Array(result.keys)

                            //get exchange rate date
                            if let dateIndex = keys.firstIndex(of: "date"),
                               let sDate = result[keys[dateIndex]] as? String, keys.count == 2 {

                                // was there a change in date?
                                if  storedDate != sDate {

                                    // get exchange rate base
                                    keys.remove(at: dateIndex)
                                    let base = keys.first!
                                    
                                    // is rateArray date different from the new data we just received?
                                    if sDate != storedDate {
                                        moneyRates.date = sDate   <- error here
                                        moneyRates.base = base
                                        moneyRates.rates = result[base] as! [String : Double]

                                        // don't update if new data stream is zero
                                        if moneyRates.rates["eur"] != 0.0 && moneyRates.rates["chf"] != 0.0 && moneyRates.rates["gbp"] != 0.0 { <- error here

                                            // set last update date to today
                                            UserDefaults.standard.setValue(sDate, forKey: StorageKeys.upd.rawValue) // this is storedDate

                                            self.gotNewRates = true
                                            getRates(baseCur: baseCur)
                                        }
                                    } else {
                                        print("Data not stored: \(sDate)")

                                    }
                                }
                            }
                        } catch {
                            print(error.localizedDescription)
                        }
                    }
                }.resume()
            }
        }
    }

解决思路

1. 让相关类型符合Sendable协议

  • 自定义GetRates结构体需要显式声明符合Sendable(其内部属性String、[String:Double]本身都是Sendable类型):
    struct GetRates: Sendable {
        var date: String
        var base: String
        var rates: [String: Double]
    }
    
  • 让GetRatesModel显式符合Sendable,@Observable类在Swift 6中默认支持Sendable,但显式声明更清晰:
    @Observable final class GetRatesModel: Sendable {
        // 现有代码保持不变
    }
    

2. 替换回调式URLSession为异步API

当前使用的dataTask(with:completionHandler:)是回调式API,容易引发Sendable问题。改用Swift原生异步data(from:)方法,避免闭包捕获self,同时更贴合Swift 6并发模型:

func checkForUpdates(baseCur: String) async -> Bool {
    guard await monitor.isConnected() else {
        return false
    }
    
    let date = Date.now
    let todayDate = date.formatted(.iso8601.year().month().day().dateSeparator(.dash))
    let storedDate = UserDefaults.standard.string(forKey: StorageKeys.upd.rawValue)
    
    guard storedDate == nil || todayDate != storedDate else {
        return false
    }
    
    let rand = Int.random(in: 1000..<10000)
    let sRand = String(rand)
    let requestType = ".json?rand=" + sRand
    let baseUrl = "https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/"
    
    guard let url = URL(string: baseUrl + baseCur + requestType) else {
        print("Invalid URL")
        return gotNewRates
    }
    
    do {
        let (data, _) = try await URLSession.shared.data(from: url)
        let result = try JSONSerialization.jsonObject(with: data) as! [String:Any]
        
        var keys = Array(result.keys)
        guard let dateIndex = keys.firstIndex(of: "date"),
              let sDate = result[keys[dateIndex]] as? String,
              keys.count == 2,
              storedDate != sDate else {
            print("Data not stored: \(sDate ?? "unknown")")
            return false
        }
        
        keys.remove(at: dateIndex)
        guard let base = keys.first,
              let rates = result[base] as? [String: Double] else {
            return false
        }
        
        guard rates["eur"] != 0, rates["chf"] != 0, rates["gbp"] != 0 else {
            return false
        }
        
        moneyRates.date = sDate
        moneyRates.base = base
        moneyRates.rates = rates
        
        UserDefaults.standard.setValue(sDate, forKey: StorageKeys.upd.rawValue)
        gotNewRates = true
        getRates(baseCur: baseCur)
        
        return true
    } catch {
        print(error.localizedDescription)
        return false
    }
}

3. 解决@main中的数据竞争警告

在MyApp的task中,通过MainActor.run确保ViewModel的调用在主线程执行,避免跨线程访问风险:

@main
struct MyApp: App {
    @State private var vm = GetRatesModel()
    @Environment(\.scenePhase) private var scenePhase
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .task(id: scenePhase) {
                    switch scenePhase {
                    case .active:
                        await MainActor.run {
                            await vm.checkForUpdates(baseCur: base.baseCur.baseS)
                        }
                    case .background, .inactive:
                        break
                    @unknown default: break
                    }
                }
        }
        .environment(vm)
    }
}

或者直接给checkForUpdates方法标记@MainActor,确保方法入口在主线程:

@MainActor
func checkForUpdates(baseCur: String) async -> Bool {
    // 异步代码可保留,async方法会自动切换线程,@MainActor确保入口在主线程
}

4. 关于ContentView中调用的疑问

将调用移到ContentView的onAppear的task中是可行的,能让调用时机与视图生命周期绑定,但这只是调整调用时机,无法从根本上解决Sendable错误,需要配合上述方案一起使用。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 04:43:09