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()
}
}
CLGeocoder().reverseGeocodeLocationis an async method and so isself.httpRequestor.populationRateForCurrentLocation. Neither of those 2 are guaranteed to execute theircompletionclosures 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.asyncinside the completion handler when you are accessing@MainActortypes/properties.receive(on:)is a Combine method defined onPublisher, but you aren't usingCombine, so you can't use that.Wrapping the property updates in
DispatchQueue.main.asyncis the correct way to solve this issue.