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.
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
}
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.")
}
})
}
}
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)
}
}
}
}
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().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
}
}
}