admin管理员组

文章数量:1336304

I have an app that reads in currency exchange rates at startup. The data is read into the array moneyRate, and only select currency exchange rates are copied to rateArray, which is used throughout the app.

With the update to Swift 6.0, I am seeing some errors and warnings. I have two errors in GetRatesModel where I am writing the date, base currency, and exchange rates into moneyRates:

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

When the @preconcurrency macro is added to the top of the viewModel GetRatesModel I get a warning in the startup file @main Sending 'self.vm' risks causing data races (Sending main actor-isolated 'self.vm' to non-isolated callee risks data races).

I have tried putting the viewModel into an actor (the warnings and errors went away except for a problem displaying the release date), but I noticed on Hacking with Swift that this isn't a recommended solution. I also looked into using an actor as an intermediary between the caller and callee to pass the home currency into checkForUpdates without success.

In @main it looks like the compiler is complaining about checkForUpdates being non-isolated. Would it help if the exchange rate viewModel was called from onAppear (within a task) in ContentView()?

Any ideas for resolving these sendable errors are much appreciated.

Below is the code for MyApp and GetRatesModel

I'm working on an update to an asynchronous URLsession API.

 @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 = "/@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()
            }
        }
    }


I have an app that reads in currency exchange rates at startup. The data is read into the array moneyRate, and only select currency exchange rates are copied to rateArray, which is used throughout the app.

With the update to Swift 6.0, I am seeing some errors and warnings. I have two errors in GetRatesModel where I am writing the date, base currency, and exchange rates into moneyRates:

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

When the @preconcurrency macro is added to the top of the viewModel GetRatesModel I get a warning in the startup file @main Sending 'self.vm' risks causing data races (Sending main actor-isolated 'self.vm' to non-isolated callee risks data races).

I have tried putting the viewModel into an actor (the warnings and errors went away except for a problem displaying the release date), but I noticed on Hacking with Swift that this isn't a recommended solution. I also looked into using an actor as an intermediary between the caller and callee to pass the home currency into checkForUpdates without success.

In @main it looks like the compiler is complaining about checkForUpdates being non-isolated. Would it help if the exchange rate viewModel was called from onAppear (within a task) in ContentView()?

Any ideas for resolving these sendable errors are much appreciated.

Below is the code for MyApp and GetRatesModel

I'm working on an update to an asynchronous URLsession API.

 @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/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()
            }
        }
    }


Share Improve this question asked Nov 19, 2024 at 23:51 Galen SmithGalen Smith 3876 silver badges18 bronze badges 2
  • Make the ViewModel with MainActor and switch to async/await. The old completion handler methods are not necessary or compatible with the newest swift6 standards. Adopting Swift6 means that you have to adopt all the newest techniques. – lorem ipsum Commented Nov 20, 2024 at 10:58
  • @State is not for objects, it should hold the data that is the result of calling the async function from .task. It is like a memory leak to init an object in @State because a new one is init on the heap and lost every time this View is init. Unnecessary heap allocations should be avoided because they slow down SwiftUI. @StateObject prevents this problem but .task replaces the need for it since it gives you the same reference semantics of an object. – malhal Commented Nov 20, 2024 at 12:26
Add a comment  | 

2 Answers 2

Reset to default 2

These errors are quite clear, if you look into dataTask completion, you could see it's marked with @Sendable:

open func dataTask(with request: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionDataTask

However, the GetRatesModel is non-sendable. That's why it's throwing an error. You can make the model conform to Sendable to resolve the error by:

@Observable final class GetRatesModel: Sendable {
    ...
}

Now the error within the completion has been addressed. But another error occurs for these stored properties, which looks like this:

Stored property 'currencyCode' of 'Sendable'-conforming class 'GetRatesModel' is mutable

Because Sendable indicates the types that are safe to share concurrently, but these properties did not have any synchronization. Thus, you can try one of these approaches:

  1. Make the model fully conform to Sendable. However, this requires value types, an actor, or immutate classes. So, in this case it should be:
@Observable final class GetRatesModel: Sendable {

    @ObservationIgnored private let currencyCode: String
    @ObservationIgnored private let rate: Double
    @ObservationIgnored private let storedDate: String?

    nonisolated init(currencyCode: String?, rate: Double?, storedDate: String?) {
        self.currencyCode = currencyCode
        self.rate = rate
        self.storedDate = storedDate
    }
}

  1. Make the mode isolate with global actors. I would use @MainActor, or you can create a new @globalActor one.
@MainActor @Observable final class GetRatesModel {
    @ObservationIgnored var currencyCode: String?
    ...
}

And you can keep these stored properties as var as it's. Because the entire GetRatesModel is now isolated with MainActor. Whenver checkForUpdates gets called, it will execute on the main thread.

func checkForUpdates(baseCur: String) async -> (Bool) {
    //<- Main
    URLSession.shared.dataTask(with: request) { //<- Background
        //<- Main
    }
}

  1. Mark the model with @unchecked and provide an internal locking mechanism, maybe with DispatchQueue
@Observable final class GetRatesModel: @unchecked Sendable {
    @ObservationIgnored var currencyCode: String?
    ...
    private let serialQueue = DispatchQueue(label: "internalGetRatesModelQueue")
    
    func updateCurrencyCode(_ code: String) {
        serialQueue.sync {
            self.currencyCode = code
        }
    }
}

Site note: there is a variant of URLSession.shared.dataTask to support async await. I would refactor it to:

let (data, response) = try await URLSession.shared.data(for: request)

Swift 6 enforces correct structure you will not be able to use custom view model objects anymore and especially not ones with async funcs that corrupt shared mutable state. The async func should be in a struct not a class and it should return a result you can set on a State. In SwiftUI the view structs are the view models so are the place to hold the view data not objects.

本文标签: swiftuiResolving Concurrency Sendable ErrorsStack Overflow