Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Forcing code to wait for object instantiation before continuing

I've written a class, UserLocation (below), which uses CoreLocation to get the user's current place (string) & latitude and longitude (also a string "lat,long" for passing to APIs).

My code below works, but when I initialize the class, the rest of my code doesn't wait for the init to finish before moving on, so I miss the opportunity to assign the location-related values that I'm trying to retrieve.

Is this a sound approach (or should I think about some other more "MVC" organization), and if it is, how can I get my code to wait for the initialization (with location find & reverse geocoding) to finish before moving on. Is there a way to put the code below the initialization into some sort of @escaping closure that's specified in the class's init? I'm new to swift so thanks for your kind advice.

In ViewController.swift's viewDidAppear():

let userLocation = UserLocation() // initializes properly but code below doesn't wait.
locationsArray[0].name = userLocation.place
locationsArray[0].coordinates = userLocation.coordinates

And my UserLocation.swift class:

import Foundation
import CoreLocation

class UserLocation {
    var place = ""
    var coordinates = ""

    let locationManager = CLLocationManager()
    var currentLocation: CLLocation!

    init() {
        returnResults()
    }

    func returnResults () {
        getUserLocation { placemark in
            if placemark != nil {
                self.place = (placemark?.name)!
                self.coordinates = "\((placemark?.location?.coordinate.latitude)!),\((placemark?.location?.coordinate.longitude)!)"
            } else {
                print("Error retrieving placemark")
            }
        }
    }

    func getUserLocation(completion: @escaping (CLPlacemark?) -> ()) {
        var placemark: CLPlacemark?

        locationManager.requestWhenInUseAuthorization() 

        if (CLLocationManager.authorizationStatus() == CLAuthorizationStatus.authorizedWhenInUse ||
            CLLocationManager.authorizationStatus() == CLAuthorizationStatus.authorizedAlways) {
            currentLocation = locationManager.location

            let geoCoder = CLGeocoder()
            geoCoder.reverseGeocodeLocation(currentLocation) { (placemarks, error) -> Void in

                if error != nil {
                    print("Error getting location: \(error)")
                    placemark = nil
                } else {
                    placemark = placemarks?.first
                }
                completion(placemark)
            }
        }
    }
}

extension CLPlacemark {
    var cityState: String {
        var result = ""
        switch (self.locality, self.administrativeArea, self.country) {
        case (.some, .some, .some("United States")):
            result = "\(locality!), \(administrativeArea!)"
        case (.some, _ , .some):
            result = "\(locality!), \(country!)"
        default:
            result = name ?? "Location Unknown"
        }
        return result
    }
}
like image 345
Gallaugher Avatar asked Nov 23 '25 16:11

Gallaugher


1 Answers

This is not necessarily a Swift issue. Your problem is caused by the fact that returnResults executes the variables setup in an async manner because it calls an async function - getUserLocation, which is async as reverseGeocodeLocation is async (this is how CoreLocation works - you don't get the location synchronously, but in a callback).

You don't want to wait for returnResults to execute the callback, as this would mean blocking the main thread while CoreLocation initializes and tries to determine the location. Instead you should follow the async pattern by using a completion block that returnResults can use to signal the completion of the location retrieval.

An example for the above would be:

class UserLocation {
    var place = ""
    var coordinates = ""

    let locationManager = CLLocationManager()
    var currentLocation: CLLocation!

    init() {
       // don't call anymore from here, let the clients ask for the locations 
    }

    // This was renamed from returnResults to a more meaningful name
    // Using the Bool in the completion to signal the success/failure
    // of the location retrieval
    func updateLocations(withCompletion completion: @escaping (Bool) -> Void) {
        getUserLocation { placemark in
            if placemark != nil {
                self.place = (placemark?.name)!
                self.coordinates = "\((placemark?.location?.coordinate.latitude)!),\((placemark?.location?.coordinate.longitude)!)"
                completion(true)
            } else {
                print("Error retrieving placemark")
                completion(false)
            }
        }
    }
...

You can then modify the called code to something like this:

let userLocation = UserLocation()
userLocation.updateLocations { success in
    guard success else { return }
    locationsArray[0].name = userLocation.place
    locationsArray[0].coordinates = userLocation.coordinates
}

You don't block the main thread, and you execute the appropriate code when the location is available.

like image 195
Cristik Avatar answered Nov 26 '25 10:11

Cristik