I have a search bar in my application that the user can type an address into, and it will come up with the geocoded result. The result updates as the user types, according to the following code:
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
...
if (self.geocoder.geocoding) [self.geocoder cancelGeocode];
[self.geocoder geocodeAddressString:searchText completionHandler:^(NSArray *placemarks, NSError *error) {
if (error != nil) {
NSLog(#"ERROR during geocode: %#", error.description);
return;
}
//update the view
}];
}
This works for the first few characters the user enters into the search field. However, after the user types more characters repeatedly, the geocoder starts giving the following error (which I know means that there was a problem with the network):
ERROR during geocode: Error Domain=kCLErrorDomain Code=2 "The operation couldn’t be completed. (kCLErrorDomain error 2.)"
The geocoder does not work again until the entire ViewController is reloaded. Why could this be happening, and what can I do to resolve it?
I believe the reason is the following:
Apple's geocoder does not answer every request in the same way. Instead, the first requests from a certain device are answered quickly, but if the device has sent say 100 requests or more, the answers arrive slower and slower or requests are not answered at all, which might cause your error.
When you reload the view controller, this simply takes time, and the geocoding server is more willing to answer again.
Essentially, you cannot do anything about it, since the geocoder sever wants to protect itself from being overloaded by requests from a single device. You simply had to limit the number of requests that you send there.
BTW: The docs say "you should not send more than one geocoding request per minute".
Note that this same error is returned when the device is offline.
I had this problem while picking location for messenger application.
My solution was to introduce delay of 3 seconds, after user stop panning map, before geocoder call. To ensure that user want exactly that location.
I was using 3 delegate methods
func mapView(_ mapView: GMSMapView, willMove gesture: Bool)
func mapView(_ mapView: GMSMapView, didChange position: GMSCameraPosition)
func mapView(_ mapView: GMSMapView, idleAt position: GMSCameraPosition)
And I was calling the reverse geolocation API in each of the methods. I got triggered the error message.
The error mainly because you are requesting the reverse geolocation API multiple times and more frequently.
How?
-> When you are about to start dragging, the first delegate method fires
-> When I was dragging the view, the camera is being changed, so the second delegate method is being fired and requesting geolocation API
-> When the camera is idle, the third delegate method is fired.
For my case, I had to show the location data in a label, like Uber set on the map, and I analyzed I need the data actually when the camera position is idle. Like I want to get the data of 10KM distance place, do I need the intermediate 9KM data?
so I removed the geolocation call from the first and second delegate method and kept only in the 3rd one. I was setting Loading.. in the label when the delegate methods got fired.
Fetching data in the background thread, because I don't want to hang up the main thread for this.
Also kept a 1-second delay before fetching, just for keeping a separation between the 2 API calls.
Related
Important Note: I am aware that I could simply call my callback function inside didUpdateLocations and achieve what I want. Unfortunately, this route cannot be taken because of some pre-existing design decisions that were made in this project (which I have no influence over).
I need to write a function that fires the first time the user's coordinates update, and then passes those coordinates to a completion handler. In my case, that completion handler is a function called fetchCountry(fromLocation: CLLocation) which returns the country corresponding to the CLLocation given.
In other words, I want to write a function similar to didUpdateLocations, with the capability of calling a completion handler after those updates have been received:
func getUserLocation(callback: #escaping (CLLocation?) -> Void) {
// wait until user's location has been retrieved by location manager, somehow
// prepare output for callback function and pass it as its argument
let latitude = manager.location!.coordinate.latitude
let longitude = manager.location!.coordinate.longitude
let location = CLLocation(latitude: latitude, longitude: longitude)
callback(location)
}
In short, getUserLocation is just a wrapper for didUpdateLocations but I am really not sure how I would go about writing this function so that it achieves what I want.
My greater goal here is to show the user a local notification only if they are in a certain country (e.g. United States) upon launching the app. It is a hard requirement for my application to make the decision of scheduling/not scheduling this notification inside AppDelegate.swift, but this decision cannot be made until the user's location has been retrieved. I plan to use getUserLocation inside the appDelegate like this:
I hope that I have conveyed clearly that I am looking to achieve this using a function with a completion handler. Here is what I would like my code to do (i.e. my use case), inside AppDelegate.swift:
// inside didFinishLaunchingWithOptions
UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .alert, .sound]) { (granted, error) in
if granted {
// this is the use case of the function I am trying to write
LocationManager.shared.getLocation(completion: { location in
// fetches the country from given location using reverse geo-coding
LocationManager.shared.fetchCountry(from: location, completion: { country in
if country == "United States" {
let notification = LocalNotification()
notificationManager.schedule(notification: notification)
}
})
})
}
}
Edited the whole answer. You would need to use a synchronizing api (OperationQueue, DispatchQueue, etc) because your CLLocationManager is already fetching even before getUserLocation is called. Callbacks alone can't handle this that's why I removed that option already. For this case, I used DispatchQueue because I prefer using it, to each their own.
class LocationManager: NSObject, CLLocationManagerDelegate{
static let shared: LocationManager()
private let privateQueue = DispatchQueue.init("somePrivateQueue")
private var latestLocation: CLLocation!{
didSet{
privateQueue.resume()
}
}
func getUserLocation(queue: DispatchQueue, callback: #escaping (CLLocation?) -> Void) {
if latestLocation == nil{
privateQueue.suspend() //pause queue. wait until got a location
}
privateQueue.async{ //enqueue work. should run when latestLocation != nil
queue.async{ //use a defined queue. most likely mainQueue
callback(self.latestLocation)
//optionally clear self.latestLocation to ensure next call to this method will wait for new user location. But if you are okay with a cached userLocation, then no need to clear.
}
}
}
func fetchCountry(from currentLocation: CLLocation, completion: ) //you already have this right?
#objc func locationManager(_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]){
latestLocation = locations.last! //API states that it is guaranteed to always have a value
}
}
I also agree on using a 3rd party library if possible. Those code would be guaranteed to be unit-tested and handle edge cases. Whereas, my above code could have a bug (none that I know of btw)
From your AppDelegate code I can assume that you are determining the country in the LocationManager class only. I would suggest to remove the call back from the getUserLocation() function and create a different function named postLocalNotification() in the AppDelegate to just post the local notification.
When you start fetching the user location the didUpdateLocation will be called in which you should call the fetchCountry() with the latest location. If the fetched country is proper and you want to post the local notification get the appelegate object and call the function which will post the notification as below
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.postLocalNotification()
Hope this helps.
Did you try to use PromiseKit+CoreLocation?
It provides CLLocationManager.requestLocation(authorizationType:satisfying:). If you don't want to import all the PromiseKit framework (which is great & avoid such completion chain), you can copy its code. They did exactly what you want: wrapping the CoreLocation request in a function.
I don't know what your special requirements are so this is general advice.
It sounds like you should set up a second location manager for your own use. Set it up in the app delegate and put its delegate callbacks in there, separate from the main location manager.
Don't try to delay willFinishLaunchingWithOptions from finishing. Depending on your requirements you might have to move any UI setup code to your own callback to set up the interface after the country is determined. I would even consider showing a different UI while you're doing this location and notification set up, then swap it out for the main UI when your have notification permission, location permission and the country.
Realise that the first location update you get through didUpdateLocations can be inaccurate. Check its distance accuracy but also check its timestamp and discard any update that's old. For your purposes (country accuracy) that probably means older than an hour. You're only really considering the use case where your app was opened for the first time after a user gets off a plane coming from another country. For accuracy, if the timestamp is recent, anything under 3000m or 5000m will be fine for that level.
Because the required accuracy is so low the location will be coming from cell tower triangulation. It should be fast (maybe within 2-5 seconds).
The one thing I'd be careful about is that your location manager will have to request location permissions while the main location manager does the same thing. I don't know how requesting permissions twice like that works.
I'd also separate fetching the country and getting location from the notification permissions.
Some more general advice: Your use case looks like it's handled in the WWDC session for Advanced NSOperations. The speaker handles cases where you need several things to be set up before the next part can move on. There's a location use case and a permissions use case in there too, one depending on the other.
As my apps enables the user to get their location every now and then, I could really use the ability to get the location in a completion block. At the moment I've set up a notification using
NSNotificationCenter.defaultCenter().postNotificationName("functionToRun", object: nil)
When the user request the location, I just run this function
func getLocation() -> CLLocation {
locationManager.requestLocation()
return location
}
However this is updating the location which can take a while, and just returning the latest location, can I implement this in a completion block, so I can get the actual location?
CLLocationManager doesn't exactly work this way but you could easily use something like the CLLocationManager-blocks cocoa pod to get this functionality. Just read through the documentation to make sure it behaves the way you expect it to.
So I understand that geocoding in iOS is an asynchronous method, and right now I'm working on an app that accesses a list of address from Parse (for restaurants) and performs forward geocoding on each address. Ideally I'd like this operation to be performed before a table is populated after a couple days of struggles this is just not happening for me.
My question is how do I get an iteration of forward geocoding to be completed prior to anything else happening in my app?
I've trying looking into grand central dispatch methods and I tried following this tutorial but I have got no luck:
http://www.raywenderlich.com/79150/grand-central-dispatch-tutorial-swift-part-2
Here is my code:
As you will see I'm trying to put found CLLocations in
var storeDict:NSDictionary = [CLLocation:PFObject]()
override func queryForTable() -> PFQuery! {
var query:PFQuery = PFQuery(className: self.parseClassName)
query.whereKey("Food", equalTo: foodName)
var prices = query.findObjects()
var i = 0
println(prices.count)
let geoCoder = CLGeocoder()
for price in prices {
var location:String = price.objectForKey("Address") as String
geoCoder.geocodeAddressString(location, completionHandler:
{(placemarks: [AnyObject]!, error: NSError!) in
if error != nil {
println("Geocode failed with error: \(error.localizedDescription)")
} else if placemarks.count > 0 {
var placemark = placemarks[0] as CLPlacemark
var location = placemark.location
var coordinateLocation:CLLocation = CLLocation(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude) as CLLocation
print(price)
print(coordinateLocation)
self.Restaurant[coordinateLocation] = price as? PFObject
print(i)
i++
}
})
println("check")
}
return query
}
First, you might want to notify the user that they can't do anything in the app until the data has finished being loaded (and geocoded). You could add a semi-transparent view with a spinning wheel over it to prevent the user from interacting with the app.
The table should have absolutely no idea where you are in the process -- whether you've started geocoding, finished geocoding, or how long ago you ran the geocoding. It should have no idea whether you even queried to get the restaurants. All it knows is its datasource, and if that datasource has objects, then it will use it to populate the rows in the table.
Say the datasource is an array of Restaurant objects. If the array is empty, then the table will be empty. You can do KVO on the array, so that whenever the datasource is updated, reloadData will be called on the tableView.
Now you've separated out the table as a separate problem that you've handled. Not onto querying and geocoding.
Currently, you're not querying Parse in background with block, but you're literally halting the program until the query finishes. I understand why you're electing to do that since you're concerned about doing nothing until the query finishes, but it would be a lot better to execute in background.
Once the query finishes, you loop through the resulting objects and geocode one by one. Just a word of caution, Apple does not allow you to geocode a ton of objects at a time -- they will throttle you, so I would limit the query to only return the amount of objects you need. When an object is finished being geocoded, add it to the datasource. This will trigger the reload of the table, and your data will appear.
Say now that you queried 20 objects. Each time the geocoding completes, your tableview will be reloaded. It might be better to wait until the geocoding completes on all 20 objects before calling reload. You can actually add each geocode operation onto a queue with each operation adding their geocoded object to some temporary array, and then add an operation that updates your datasource with the temporary array. At that moment, the table will be updated with the new data. Note that the downside of doing it this way is that some addresses might take longer the geocode than others, so rather than displaying whatever data it was able to geocode thus far, it will wait until everything has been fully geocoded.
Lastly, you could have the query run in background and have the geocoding occur in its completion block. If the view just loaded for the first time, it can show the spinning wheel until the datasource is updated. When the datasource is updated, the spinning wheel is removed.
So I am using parse to get an update of the users location and data from that query like so:
-(void) getUserLocationAndData:(BOOL) showProgress
{
[PFGeoPoint geoPointForCurrentLocationInBackground:^(PFGeoPoint *geoPoint, NSError *error)
{
if (!error)
{
NSLog(#"Got current location - Zooming Map!");
CLLocationCoordinate2D zoomLocation;
zoomLocation.latitude = geoPoint.latitude;
zoomLocation.longitude = geoPoint.longitude;
[self getNearByData:geoPoint];
}
else
{
[[[UIAlertView alloc] initWithTitle:#"Unable to get Current Location" message:#"This app needs your current location in order to locate near by data. Please check your internet connection and make sure you enabled access to the location information." delegate:self cancelButtonTitle:#"Ok" otherButtonTitles:nil] show];
}
}];
}
When the app first launches the user location shows up, the data is pulled and everything works. If you wait about 15 seconds and don't move the user location icon turns gray (see image) and any updates result in a failure. If I reload the view it works again but the same thing happens.
I am not using a CLLocationManager because I do not need to constantly pull the data near the user. Just every so often or on demand by the user.
I have also noticed if I wait long enough the user location goes Blue again and all seems to work. What is causing this timeout? Can I set this timeout, or do I need to just use a CLLocationManager to have any control of this? Is Parse just timing out with an internal CLLocationManager or something?
Thanks for the help!
So evidently when you use the Parse function: geoPointForCurrentLocationInBackground Parse will take over with it's own internal CLLocationManager. After it does it's initial thing it stops updating the location (or possibly only updates the user location if the user moves enough.. again just a guess here.. and if that feature is supported on the device).
The solution is to create your own CLLocationManager in your view controller and instead of calling
geoPointForCurrentLocationInBackground
You simply store the updated user location and call:
PFGeoPoint *usersGeoPoint = [PFGeoPoint geoPointWithLocation:usersLastKnownLocation];
And now we can use that to query near by data. We are now responsible for starting and stopping the updating of the user location. I strongly recommend using this post to setup your own LocationManager and be sure to scroll down to the updated answers for more up to date info.
How can I get current location from user in iOS
After reading about the geoPointWithLocation in the Parse docs I made the conclusions above. Please read it for yourself (I know it's really hard to find and just one line...)
i have to execute a high number of reverseGeocodeLocation request,
i use this method for doing that:
for (Photo *photo in arrayWhitPicture) {
CLGeocoder *geocoder = [[CLGeocoder alloc] init];
[queue addOperationWithBlock:^{
[geocoder reverseGeocodeLocation:[[CLLocation alloc] initWithLatitude:[photo.latitude doubleValue] longitude:[photo.longitude doubleValue]] completionHandler:^(NSArray *placemarks, NSError *error) {
if (error){
NSLog(#"Geocode failed with error: %#", error);
return;
}
CLPlacemark *myPlacemark = [placemarks objectAtIndex:0];
NSString *city = myPlacemark.locality;
NSLog(#"My country code: %#", city);
}];
}];
}
this code actual work, but the problem is that some of these request (like half) get this error:
Error Domain=kCLErrorDomain Code=2
after a few research i think this happen because i do a lot of request in short amount of time in fact the apple documentations say:
Send at most one geocoding request for any one user action.
When you want to update the user’s current location automatically
(such as when the user is moving), issue new geocoding requests only
when the user has moved a significant distance and after a reasonable
amount of time has passed. For example, in a typical situation, you
should not send more than one geocoding request per minute.
so my question is: my error is really caused by the fact that i do a lot of request, and in that case what can i do to resolve this problem, do you know other system apart to use the reverseGeocoding?
I have had exactly this problem, and my solution was to throttle down the number of reverse geocode queries that I performed (i.e. temporarily suspend the queue if already processed a whole bunch). This worked so well that I did not have to implement plan B, which was to switch to a different service.
See for example this question for discussion of alternate services. Google has a similar limit of 2500 requests per API key and 24 hour period. There is also Bing.
Apple most definitely limits how many geocoding requests you can issue at a time. Other users are reporting that the limit is around 50, though that could change at any time. The recommendation seems to be to do the geocoding in batches and to issue only one batch at a time, starting each batch only after the previous one completes.
Apple limits to perform number of Reverse GeoCode requests your application can make at a time. Sometimes I have seen this limited to ONE.
The solution is to implement your own Reverse Geocoder Queue (you can implement it as a separate class), in which you can add all of your requests. This queue need to execute one request at a time and after first is done, execute next. You can add callback blocks to notify you once the reverse geocoding is done for each request.
Example API in the Reverse Geocoder queue class can be like:
- (void) reverseGeocodeLocation: (CLLocation *) location completion: (CLGeocodeCompletionHandler) completionHandler
{
// Create some queue (NSMutableArray) in the class
// Create some ReverseGeoLocationObject with location and completionHandler as members
// Add ReverseGeoLocationObject to queue
// Check is queue is not already processing. If NO then process next request. You have have API named processNextRequest which you can call here (put code you posted in this API for single request)
}
Also call processNextRequest when CLGeocoder returns.