I have a list of locations (about 30 elements):
var locations: [CLLocation] = [
CLLocation(latitude: 45.471172, longitude: 9.163317),
...
]
My purpose is to get street names from that list, so I decided to use CLGeocoder().
I call a function inside a viewDidLoad(), and every location is processed by lookUpCurrentLocation().
override func viewDidLoad() {
super.viewDidLoad()
for location in locations {
lookUpCurrentLocation(location: location, completionHandler: { streetName in
print(streetName)
})
}
}
func lookUpCurrentLocation(location: CLLocation, completionHandler: #escaping (String?) -> Void) {
CLGeocoder().reverseGeocodeLocation(location, completionHandler: { (placemarks, error) in
let placemark = placemarks?[0]
completionHandler(placemarks?[0].name)
})
}
My problem:
when the app starts, it prints a list of nil or only first two nil and the others street names.
terminal image 1
terminal image 2
I aspect to see the whole list processed without any nil.
Any hints?
As Leo said, you don’t want to run the requests concurrently. As the documentation says:
After initiating a reverse-geocoding request, do not attempt to initiate another reverse- or forward-geocoding request. Geocoding requests are rate-limited for each app, so making too many requests in a short period of time may cause some of the requests to fail. When the maximum rate is exceeded, the geocoder passes an error object with the value CLError.Code.network to your completion handler.
There are a few approaches to make these asynchronous requests run sequentially:
The simple solution is to make the method recursive, invoking the next call in the completion handler of the prior one:
func retrievePlacemarks(at index: Int = 0) {
guard index < locations.count else { return }
lookUpCurrentLocation(location: locations[index]) { name in
print(name ?? "no name found")
DispatchQueue.main.async {
self.retrievePlacemarks(at: index + 1)
}
}
}
And then, just call
retrievePlacemarks()
FWIW, I might use first rather than [0] when doing the geocoding:
func lookUpCurrentLocation(location: CLLocation, completionHandler: #escaping (String?) -> Void) {
CLGeocoder().reverseGeocodeLocation(location) { placemarks, _ in
completionHandler(placemarks?.first?.name)
}
}
I don’t think it’s possible for reverseGeocodeLocation to return a non-nil, zero-length array (in which case your rendition would crash with an invalid subscript error), but the above does the exact same thing as yours, but also eliminates that potential error.
An elegant way to make asynchronous tasks run sequentially is to wrap them in an asynchronous Operation subclass (such as a general-purpose AsynchronousOperation seen in the latter part of this answer).
Then you can define a reverse geocode operation:
class ReverseGeocodeOperation: AsynchronousOperation {
private static let geocoder = CLGeocoder()
let location: CLLocation
private var geocodeCompletionBlock: ((String?) -> Void)?
init(location: CLLocation, geocodeCompletionBlock: #escaping (String?) -> Void) {
self.location = location
self.geocodeCompletionBlock = geocodeCompletionBlock
}
override func main() {
ReverseGeocodeOperation.geocoder.reverseGeocodeLocation(location) { placemarks, _ in
self.geocodeCompletionBlock?(placemarks?.first?.name)
self.geocodeCompletionBlock = nil
self.finish()
}
}
}
Then you can create a serial operation queue and add your reverse geocode operations to that queue:
private let geocoderQueue: OperationQueue = {
let queue = OperationQueue()
queue.name = Bundle.main.bundleIdentifier! + ".geocoder"
queue.maxConcurrentOperationCount = 1
return queue
}()
func retrievePlacemarks() {
for location in locations {
geocoderQueue.addOperation(ReverseGeocodeOperation(location: location) { string in
print(string ?? "no name found")
})
}
}
If targeting iOS 13 and later, you can use Combine, e.g. define a publisher for reverse geocoding:
extension CLGeocoder {
func reverseGeocodeLocationPublisher(_ location: CLLocation, preferredLocale locale: Locale? = nil) -> AnyPublisher<CLPlacemark, Error> {
Future<CLPlacemark, Error> { promise in
self.reverseGeocodeLocation(location, preferredLocale: locale) { placemarks, error in
guard let placemark = placemarks?.first else {
return promise(.failure(error ?? CLError(.geocodeFoundNoResult)))
}
return promise(.success(placemark))
}
}.eraseToAnyPublisher()
}
}
And then you can use a publisher sequence, where you specify maxPublishers of .max(1) to make sure it doesn’t perform them concurrently:
private var placemarkStream: AnyCancellable?
func retrievePlacemarks() {
placemarkStream = Publishers.Sequence(sequence: locations).flatMap(maxPublishers: .max(1)) { location in
self.geocoder.reverseGeocodeLocationPublisher(location)
}.sink { completion in
print("done")
} receiveValue: { placemark in
print("placemark:", placemark)
}
}
There are admittedly other approaches to make asynchronous tasks run sequentially (often involving calling wait using semaphores or dispatch groups), but I don’t think that those patterns are advisable, so I’ve excluded them from my list of alternatives, above.
Here's an implementation using Combine, with a persistent cache. Need more intelligent cache expiry logic, etc, but it is a starting point. Patches welcome.
https://gist.github.com/lhoward/dd6b64fb8f5782c933359e0d54bcb7d3
Related
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:
Check permissions (yes, you have to do it every time, as user may take away the permissions a any point)
And tell CLLocationManager to start resolving user location
After that just listen for result via locationManager(:, 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).
I want to get a value from function. There is a block in function. When block executes the function already returns the value. I tried many different methods but they didn't help me. I used NSOperation and Dispatch. The function always returns value until execution of block.
var superPlace: MKPlacemark!
func returnPlaceMarkInfo(coordinate: CLLocationCoordinate2D) -> MKPlacemark? {
let location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
geocoder.reverseGeocodeLocation(location) { (arrayPlaceMark, error) -> Void in
if error == nil {
let firstPlace = arrayPlaceMark!.first!
let addressDictionaryPass = firstPlace.addressDictionary as! [String : AnyObject]
self.superPlace = MKPlacemark(coordinate: location.coordinate, addressDictionary: addressDictionaryPass)
}
}
return superPlace
}
You cannot simply return here as the reverseGeocodeLocation function is running asynchronously so you will need to use your own completion block:
var superPlace: MKPlacemark!
func getPlaceMarkInfo(coordinate: CLLocationCoordinate2D, completion: (superPlace: MKPlacemark?) -> ()) {
let location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
geocoder.reverseGeocodeLocation(location) { (arrayPlaceMark, error) -> Void in
if error == nil {
let firstPlace = arrayPlaceMark!.first!
let addressDictionaryPass = firstPlace.addressDictionary as! [String : AnyObject]
self.superPlace = MKPlacemark(coordinate: location.coordinate, addressDictionary: addressDictionaryPass)
completion(superPlace: superPlace)
} else {
completion(superPlace: nil)
}
}
}
This comes up over and over and over. The short answer is "you can't."
The result is not available when your function returns. The async call takes place in the background.
What you want to do is refactor your returnPlacemarkInfo function to take a completion closure.
I have been working in Objective-C lately, so my Swift is a little rusty, but it might look like this:
func fetchPlaceMarkInfo(
coordinate: CLLocationCoordinate2D,
completion: (thePlacemark: MKPlacemark?) -> () )
{
}
Then when you call it, pass in a completion closure that gets invoked once the placemark is available.
EDIT:
I wrote a demo project and posted it on Github that simulates handling an async network download. Take a look at
https://github.com/DuncanMC/SwiftCompletionHandlers
Specifically look at the method asyncFetchImage(), which does almost exactly what we are talking about: Uses an async method internally, and takes a completion block that it calls once the async load is done.
I've written a function that's supposed to return the HTML string that makes up a WKWebview. However, the completion handler is never called, and the project freezes indefinitely. I've also already adopted the WKScriptMessageHandler protocol so that's not the problem.
public func getHTML() -> String {
var result = ""
let group = DispatchGroup()
group.enter()
DispatchQueue.main.async {
self.webView.evaluateJavaScript("document.documentElement.outerHTML.toString()", completionHandler: {(html: Any?, error: Error?) in
if (error != nil) {
print(error!)
}
result = html as! String
group.leave()
})
}
group.wait()
print("done waiting")
return result
}
I've found several examples on how to get the html, like here, but I don't want to merely print, I want to be able to return its value. I'm not experienced with DispatchQueues, but I do know for that WKWebView's evaluateJavaScript completion handler always runs on the main thread
I have this method:
internal func getCoordinate( addressString : String,
completionHandler: #escaping(CLLocationCoordinate2D, NSError?) -> Void ) {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(addressString) { (placemarks, error) in
if error == nil {
if let placemark = placemarks?[0] {
let location = placemark.location!
completionHandler(location.coordinate, nil)
return
}
}
completionHandler(kCLLocationCoordinate2DInvalid, error as NSError?)
}
}
Use it like:
self.getCoordinate(addressString: "Summerville, SC", completionHandler: <#T##(CLLocationCoordinate2D, NSError?) -> Void#>)
But I want to do something like this:
let currentLocation = self.getCoordinate(addressString: "Summerville, SC", completionHandler: <#T##(CLLocationCoordinate2D, NSError?) -> Void#>)
How do I do that?
You cannot use a value from an asynchronous function just like it was a normal synchronous return value.
You have to access the value inside the closure to ensure that the asynchronous method has already finished execution. If you were trying to access that value outside the closure, you couldn't ensure in any way that the value is already set, since asynchronous methods return before they would actually finish execution, but you can use a closure to ensure this.
There are some methods (such as DispatchQueues or the use of a 3rd party framework, like PromiseKit) other than completion handlers to solve this issue, but you will never be able to use a value from an async method the same way, without any limitations as you would with a value from a synchronous method.
This is how to access the value returned from the completion handler:
self.getCoordinate(addressString: "Summerville, SC", completionHandler: { coordinate, error in
guard error == nil else {return}
//use the coordinate here
})
Just a minor improvement to your current method: instead of returning an invalid coordinate in case the geocoding was unsuccessful, I would return nil as the coordinate in this case.
I'm using DispatchGroup.enter() and leave() to process a helper class's reverseG async function. Problem is clear, I'm using mainViewController's object to call mainViewControllers's dispatchGroup.leave() in helper class! Is there a way to do it?
Same code works when reverseG is declared in the main view controller.
class Geo {
var obj = ViewController()
static func reverseG(_ coordinates: CLLocation, _ completion: #escaping (CLPlacemark) -> ()) {
let geoCoder = CLGeocoder()
geoCoder.reverseGeocodeLocation(coordinates) { (placemarks, error) in
if let error = error {
print("error: \(error.localizedDescription)")
}
if let placemarks = placemarks, placemarks.count > 0 {
let placemark = placemarks.first!
completion(placemark) // set ViewController's properties
} else {
print("no data")
}
obj.dispatchGroup.leave() // ** ERROR **
}
}
}
Function call from main view controller
dispatchGroup.enter()
Geo.reverseG(coordinates, setValues) // completionHandler: setValues
dispatchGroup.notify(queue: DispatchQueue.main) {
// call another function on completion
}
Every leave call must have an associated enter call. If you call leave without having first called enter, it will crash. The issue here is that you're calling enter on some group, but reverseG is calling leave on some other instance of ViewController. I'd suggest passing the DispatchGroup as a parameter to your reverseG method. Or, better, reverseG shouldn't leave the group, but rather put the leave call inside the completion handler that reserveG calls.
dispatchGroup.enter()
Geo.reverseG(coordinates) { placemark in
defer { dispatchGroup.leave() }
guard let placemark = placemark else { return }
// use placemark here, e.g. call `setValues` or whatever
}
dispatchGroup.notify(queue: DispatchQueue.main) {
// call another function on completion
}
And
class Geo {
// var obj = ViewController()
static func reverseG(_ coordinates: CLLocation, completion: #escaping (CLPlacemark?) -> Void) {
let geoCoder = CLGeocoder()
geoCoder.reverseGeocodeLocation(coordinates) { placemarks, error in
if let error = error {
print("error: \(error.localizedDescription)")
}
completion(placemarks?.first)
// obj.dispatchGroup.leave() // ** ERROR **
}
}
}
This keeps the DispatchGroup logic at one level of the app, keeping your classes less tightly coupled (e.g. the Geo coder doesn't need to know whether the view controller uses dispatch groups or not).
Frankly, I'm not clear why you're using dispatch group at all if there's only one call. Usually you'd put whatever you call inside the completion handler, simplifying the code further. You generally only use groups if you're doing a whole series of calls. (Perhaps you've just simplified your code snippet whereas you're really doing multiple calls. In that case, a dispatch group might make sense. But then again, you shouldn't be doing concurrent geocode requests, suggesting a completely different pattern, altogether.
Passed dispatchGroup as parameter with function call and it worked.
Geo.reverseG(coordinates, dispatchGroup, setValues)
my two cents to show how can work:
(maybe useful for others..)
// Created by ing.conti on 02/02/21.
//
import Foundation
print("Hello, World!")
let r = AsyncRunner()
r.runMultiple(args: ["Sam", "Sarah", "Tom"])
class AsyncRunner{
static let shared = AsyncRunner()
let dispatchQueue = DispatchQueue(label: "MyQueue", qos:.userInitiated)
let dispatchGroup = DispatchGroup.init()
func runMultiple(args: [String]){
let count = args.count
for i in 0..<count {
dispatchQueue.async(group: dispatchGroup) { [unowned self] in
dispatchGroup.enter()
self.fakeTask(arg: args[i])
}
}
_ = dispatchGroup.wait(timeout: DispatchTime.distantFuture)
}
func fakeTask(arg: String){
for i in 0..<3 {
print(arg, i)
sleep(1)
}
dispatchGroup.leave()
}
}