swiftswiftuiappstoragemainactor

I'm getting a purple warning when trying to save via @AppStorage inside an asynchronous call


In the code below I ask the server for the popuplation rate for the city the user is current in via HTTP request.

Everything works as expected except that I'm getting a purple warning when I save lastSearchedCity and lastSearchedPopulationRate to UserDefaults inside the http synchronous function call via @AppStorage. Again, I get the right info from the server and everything seem to be saving to UserDefaults, the only issue is the purple warning.

Purple Warning:

Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.

I tried wraping self.lastSearchedCity = city and self.lastSearchedPopulationRate = pRate inside DispatchQueue.main.async {} but I'm afraid this is more then that since the compiler suggest using the receive(on:) operator but I'm not sure how to implement it.

if let pRate = populationRate{
    self.lastSearchedCity = city // purple warning points to this line
    self.lastSearchedPopulationRate = pRate // purple warning points to this line
}

What would be the right way to solve this warning?

Core Location

    class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
        
        private let locationManager = CLLocationManager()
        
        @AppStorage("kLastSearchedCity")private var lastSearchedCity = ""
        @AppStorage("kLastSearchedPopulationRate")private var lastSearchedPopulationRate = ""
        
        @Published var locationStatus: CLAuthorizationStatus?
        
        var hasFoundOnePlacemark:Bool = false
        
        let httpRequestor = HttpPopulationRateRequestor()

        override init() {
            super.init()
            locationManager.delegate = self
            locationManager.desiredAccuracy = kCLLocationAccuracyBest
            locationManager.requestWhenInUseAuthorization()
            locationManager.startUpdatingLocation()
        }

        var statusString: String {
            guard let status = locationStatus else {
                return "unknown"
            }

            switch status {
            case .notDetermined: return "notDetermined"
            case .authorizedWhenInUse: return "authorizedWhenInUse"
            case .authorizedAlways: return "authorizedAlways"
            case .restricted: return "restricted"
            case .denied: return "denied"
            default: return "unknown"
            }
        }

        func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
            locationStatus = status
        }
        
        func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
            hasFoundOnePlacemark = false
            
            CLGeocoder().reverseGeocodeLocation(manager.location!, completionHandler: {(placemarks, error)-> Void in
                if error != nil {
                    self.locationManager.stopUpdatingLocation()
                
                if placemarks!.count > 0 {
                    if !self.hasFoundOnePlacemark{

                        self.hasFoundOnePlacemark = true
                        let placemark = placemarks![0]
                        
                        let city:String = placemark.locality ?? ""
                        let zipCode:String = placemark.postalCode ?? ""
                                                    
                        // make request
                        if city != self.lastSearchedCity{
                            // asynchronous function call
                            self.httpRequestor.populationRateForCurrentLocation(zipCode: zipCode) { (populationRate) in
                                if let pRate = populationRate{
                                    self.lastSearchedCity = city // purple warning points to this line
                                    self.lastSearchedPopulationRate = pRate // purple warning points to this line
                                }
                            }
                        }
                    }
                    self.locationManager.stopUpdatingLocation()
                }else{
                print("No placemarks found.")
                }
            })
        }
    }

SwiftUI - For reference only

    struct ContentView: View {
        @StateObject var locationManager = LocationManager() 

        @AppStorage("kLastSearchedCity")private var lastSearchedCity = ""
        @AppStorage("kLastSearchedPopulationRate")private var lastSearchedPopulationRate = ""

        var body: some View {
            VStack {
                Text("Location Status:")
                .font(.callout)
                Text("Location Status: \(locationManager.statusString)")
                .padding(.bottom)

                Text("Population Rate:")
                    .font(.callout)
                HStack {
                    Text("\(lastSearchedCity)")
                        .font(.title2)
                    Text(" \(lastSearchedPopulationRate)")
                        .font(.title2)
                }
            }
        }
    }

HTTP Request class

    class HttpPopulationRateRequestor{    
        let customKeyValue = "ryHGehesdorut$=jfdfjd"
        let customKeyName = "some-key"
        
        func populationRateForCurrentLocation(zipCode: String, completion:@escaping(_ populationRate:String?) -> () ){
            
            print("HTTP Request: Asking server for population rate for current location...")
            
            let siteLink = "http://example.com/some-folder/" + zipCode
            let url = URL(string: siteLink)
            
            var request = URLRequest(url: url!)
            request.setValue(customKeyValue, forHTTPHeaderField: customKeyName)
        
            let task = URLSession.shared.dataTask(with: request) { data, response, error in
                
                guard error == nil else {
                    print("ERROR: \(error!)")
                    completion(nil)
                    return
                }
                guard let data = data else {
                    print("Data is empty")
                    completion(nil)
                    return
                }
                let json = try! JSONSerialization.jsonObject(with: data, options: [])
                
                guard let jsonArray = json as? [[String: String]] else {
                    return
                }
                if jsonArray.isEmpty{
                    print("Array is empty...")
                    return
                }else{
                    let rate = jsonArray[0]["EstimatedRate"]!
                    let rateAsDouble = Double(rate)! * 100
                    completion(String(rateAsDouble))
                }
            }
            task.resume()
        }
    }

Solution

  • CLGeocoder().reverseGeocodeLocation is an async method and so is self.httpRequestor.populationRateForCurrentLocation. Neither of those 2 are guaranteed to execute their completion closures on the main thread.

    You are updating properties from your closure which are triggering UI updates, so these must be happening from the main thread.

    You can either manually dispatch the completion closure to the main thread or simply call DispatchQueue.main.async inside the completion handler when you are accessing @MainActor types/properties.

    receive(on:) is a Combine method defined on Publisher, but you aren't using Combine, so you can't use that.

    Wrapping the property updates in DispatchQueue.main.async is the correct way to solve this issue.

    self.httpRequestor.populationRateForCurrentLocation(zipCode: zipCode) { (populationRate) in
      if let pRate = populationRate {
        DispatchQueue.main.async {
          self.lastSearchedCity = city
          self.lastSearchedPopulationRate = pRate
        }
      }
    }