Why locationManager is not called immediately after being called in viewDidLoad

I need to get the zipCode and city from multiple locations in my app so I created the following class.


import Foundation
import CoreLocation

class MyLocationManager: NSObject, CLLocationManagerDelegate {
   
    private let locationManager = CLLocationManager()
    private var zipCode:String?
    private var city:String?
   
    static let shared = MyLocationManager()
   
    private override init() {
        super.init()
        locationManager.delegate = self
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.distanceFilter = kCLLocationAccuracyHundredMeters
        locationManager.requestWhenInUseAuthorization()
        locationManager.startUpdatingLocation()
    }
   
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        CLGeocoder().reverseGeocodeLocation(manager.location!, completionHandler: {(placemarks, error)-> Void in
            if error != nil {
                //AlertView to show the ERROR message
            }
            if placemarks!.count > 0 {
                let placemark = placemarks![0]
                self.locationManager.stopUpdatingLocation()
                self.zipCode = placemark.postalCode ?? ""
                self.city = placemark.locality ?? ""
            }else{
                print("No placemarks found.")
            }
        })
    }

    public func getZipCode()->String {
        return zipCode ?? ""
    }
   
    public func getCity()->String {
        return city ?? ""
    }
}


The issue is that the first time I call the getZipCode() or the getCity() methods I get an empty string. Here is how I'm using it in a viewController...


class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        print("Location: \(MyLocationManager.shared.getCity())")// empty
    }
   
    /// Action to test it after viewDidLoad with a button
    @IBAction func testing(_ sender: Any) {
        print("Location: \(MyLocationManager.shared.getCity())")// returns the name of the city
    }
}


In viewDidLoad I get an empty string but when I tap the testing button (second call) I do get the name of the city.


How can I make it update the city name in viewDidLoad as soon as I call MyLocationManager.shared.getCity()?

Replies

How are you testing...sim or device? Has location service already been enabled/agreed prior to running a request via code? How many other apps are using location services at the same time?

AFAICT, problem comes from the fact that uupdate of location occurs in other threads, late after init.


So, I have done this, not very clean (in particular the sleep) to call a requestLocation at the end of init.

Seems to work.


But needs to be cleaned.


import Foundation
import CoreLocation

class MyLocationManager: NSObject, CLLocationManagerDelegate {
   
    private let locationManager = CLLocationManager()
    private var zipCode:String?
    private var city:String?
    private var newLocation: CLLocation?
   
    static let shared = MyLocationManager()
   
    private override init() {
        super.init()
        locationManager.delegate = self
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.distanceFilter = kCLLocationAccuracyHundredMeters
        locationManager.requestWhenInUseAuthorization()
        locationManager.stopUpdatingLocation()     // request will restart it

        locationManager.requestLocation()
    }
   
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {     // Needed for request
        print("Some error")
    }
   
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
       
        newLocation = locations[locations.count - 1]//        print(locations.count)
        getPlacemarkFromLocation(newLocation!)

    }


     // I've isolated the func, but not really needed
    func getPlacemarkFromLocation(_ location: CLLocation) {
       
        CLGeocoder().reverseGeocodeLocation(location, completionHandler: {(placemarks, error)-> Void in
            if error != nil {
                //AlertView to show the ERROR message
            }
            if placemarks!.count > 0 {
                let placemark = placemarks![0]
                self.locationManager.stopUpdatingLocation()
                self.zipCode = placemark.postalCode ?? ""
                self.city = placemark.locality ?? ""
            }else{
                print("No placemarks found.")
            }
        })
    }


    public func getZipCode()->String {
        return zipCode ?? ""
    }
   
    public func getCity()->String {
        return self.city ?? ""
    }
}


import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        print("Location: \(MyLocationManager.shared.getCity())")// empty
    }
  
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        DispatchQueue.global(qos: .userInitiated).async {
            sleep(1)
        self.testing(self)
        }
    }
  
    @IBAction func testing(_ sender: Any) {

        print("Location: \(MyLocationManager.shared.getCity())")// returns the name of the city
    }
  
}



On simulator, I get first an empty message, then, 1 sec later, the correct city


Location:

Location: Cupertino

The didUpdateLocations takes a bit of time to respond and your print command is executed immediately. A simple approach is to place the print command in a method that you call after a delay - in ObjectiveC use:

[self performSelector:@selector(delayedStuff) withObject:nil afterDelay:0.5f];


But an even simplier approach is to place your print in viewWillAppear.

Also, the location gets better with time so you may want to look into updating the print statement when the location gets updated.

The didUpdateLocations takes a bit of time to respond and your print command is executed immediately.

Thank you for the clearification, I was wondering why if locationManager was called before my getCity method why would the getCity be called first.


A simple approach is to place the print command in a method that you call after a delay

I tried adding a 1 second delay inside getCity() method and it seem to be enough to get the location value, the issue is, how do I delay the return inside getCity()?



    public func getCity()->String {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            print("Delay: \(self.city ?? "")") // prints the city name after the 1 sec delay.
        }
        return city ?? "" // How can I delay the retrun?
    }


But an even simplier approach is to place your print in viewWillAppear.

Moving the print to viewWillAppear does nothing.


Also, the location gets better with time so you may want to look into updating the print statement when the location gets updated.

Any suggestion, using notifications?

>Also, the location gets better with time so you may want to look into updating the print statement when the location gets updated.


This is actually the better answer to your original question; go with the empty string and update it as the newer location values come in.

I never learned Swift so I can't respond specifically to your code;

In Objective C you assign a delegate:

https://developer.apple.com/documentation/corelocation/cllocationmanagerdelegate?language=objc

and respond to repeated calls to didUpdateLocations.


It appears that your code is doing that (but I don't read Swift) so ask yourself whether or not you are responding to those repeated calls.

This works but the one thing I don't like about this is that if for some reason locationManager takes longer than the delay (on this case 1 sec.), I will get an empty string. What I would like to be able to do is some how do the dealy in the MyLocationManager class so it doesn't send a value until locationManager has found a location. I'm lost. Thanks.

Yes, in some cases 1 sec is not enough.


So you could try sending a notification in didUpdateLocations and update the textField when notified.


In fact, it depends a bit on the logic of your app.

I will try to go with a notification. Thanks a lot for your suggestions.