I'm working on an App Shortcut using the new AppIntents framework in iOS 16 and I'm trying to get the user's current location, everything is enabled and set-up correctly with the permissions
func perform() async throws -> some IntentResult {
//Request User Location
IntentHelper.sharedInstance.getUserLocation()
guard let userCoords = IntentHelper.sharedInstance.currentUserCoords else { throw IntentErrors.locationProblem }
//How to wait for location??
return .result(dialog: "Worked! Current coords are \(userCoords)") {
IntentSuccesView()
}
}
And here is the IntentHelper class
class IntentHelper: NSObject {
static let sharedInstance = IntentHelper()
var currentUserCoords: CLLocationCoordinate2D?
private override init() {}
func getUserLocation() {
DispatchQueue.main.async {
let locationManager = CLLocationManager()
locationManager.delegate = self
print("FINALLY THIS IS IT")
self.currentUserCoords = locationManager.location?.coordinate
print(self.currentUserCoords)
}
}
}
extension IntentHelper: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error)
manager.stopUpdatingLocation()
}
}
Problem is, this sometimes, very rarely works, most of the times it prints nil, so how would you go about waiting for the location?
The problem is you are trying to get the location synchronously, so it only works if locationManager.location
was already not nil by the time you ask for it. Instead this operation may take time and is therefore asynchronous.
So the basic flow is like this:
CLLocationManager
to start resolving user locationlocationManager(:, didUpdateLocations:)
event of the CLLocationManagerDelegate
, which
you need to implement (in your case in the same class, as you already
implemented the failure case in extension).On top of that, you probably want to wait for location update (either coordinates or failure) inside your func perform()
.
So I would say you need to have something like this in func perform()
:
// Wait for coordinates
guard let userCoords = await IntentHelper.sharedInstance.getCurrentCoordinates() else { ... }
where the getCurrentCoordinates()
is just an async wrapper, something like:
func getCurrentCoordinates() async -> CLLocationCoordinate2D? {
await withCheckedContinuation { continuation in
getCurrentCoordinates() { coordinates in
continuation.resume(returning: coordinates)
}
}
}
while getCurrentCoordinates(callback:)
will be something like:
class IntentHelper {
var callback: ((CLLocationCoordinate2D?) -> Void)?
//...
func getCurrentCoordinates(callback: @escaping (CLLocationCoordinate2D?) -> Void) {
// Step 1: check permissions
let status = CLLocationManager.authorizationStatus()
guard status == .authorizedAlways || status == .authorizedWhenInUse else {
// you can't ask for permissions
callback(nil)
return
// Step 2: preserve callback and request location
self.callback = callback
locationManager?.requestLocation()
}
}
Now all you need to do is wait for locationManager(:, didUpdateLocations:)
or locationManager(:, didFailWithError:)
to happen:
extension IntentHelper: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
// Pass the result (no location info) back to the caller
self.callback?(nil)
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// Pass the result location back to the caller
// For simplicity lets say we take the first location in list
self.callback?(locations.first)
}
}
Note: this is a draft code, I didn't try to compile it, so you may need to fix some compilation errors.
Here's a nice walkthrough of the whole scenario (which also shows a nicer code organization (i.e. how to ask for permissions, etc).