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.
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.
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.
I'm building an iOS app, however I want to get notification which may be a local and simple notification, such that on defined area of location, such that to provided the latitude and longitude to get the area around 200meter and when the user entered in that location, it alerts the notification.
How can I schedule the location based local notification in iOS.
Take a look at what is available in the SetSDK, https://cocoapods.org/pods/SetSDK. It will allow you to get notified whenever the user arrives to a new "location" (as learned on the fly by the sdk) and then just quickly evaluate to see if it is a location you care about. So it would look something like this,
SetSDK.instance.onArrival(to: .any) { newArrival in
/* Compare the new location with the one of interest */
if newArrival.location.distance(from: placeOfInterest) < 200 {
/* do your things here */
}
}
Use a CLRegion in combination with Background Location and your app will be woken up when the user enters that region. From there you can schedule a UILocationNotification for immediate display.
I was trying out the Google's Geo fence sample project. I'm getting the entry/exit events correctly. But how can I get the current location of the user from it. I would like to keep track of user location even after entry/exit points. Please help.
The sample project is from the developer portal.
in your broadcastrecievero or service you get a GeofencingEvent , use it to get the triggering position
public class GeofenceTransitionsIntentService extends IntentService {
//[...]
#Override
protected void onHandleIntent(Intent intent) {
GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent);
if (geofencingEvent.hasError()) {
// ...
}
Location l = geofencingEvent.getTriggeringLocation()
}
//[...]
}
You'll need can get the user location from the Fused Location Provider in the normal fashion. The full tutorial is here: http://developer.android.com/training/location/retrieve-current.html
But basically, when you receive the geofence event inside your Receiver or Service, get an instance of LocationClient there, connect to it, and in the callback, onConnected(), get the last known location:
Location position = locationClient.getLastLocation();
99.9% of the time, or a little better, you'll find this location to be inside your geofence.
To continue keeping track of user location updates, follow the Receiving Location Updates: http://developer.android.com/training/location/receive-location-updates.html
I want execute a function all time while my ios App is running.
What is the class where I need write this function, in the delegate?
I'm confused because if I declared this in viewContorller and change to other viewController this break. Or there is a function like
func locationManager(manager:CLLocationManager, didUpdateLocations locations:[AnyObject]) {
that this is running all time?
Thanks!
If you are referring specifically to didUpdateLocations, this is a method that you don't call directly, but rather the OS calls to deliver location updates to you any time it receives them. While it's typically recommended that your location code is handled by some singleton somewhere to consolidate/encapsulate all the location logic, if you create a CLLocationManager and tell it to startUpdatingLocations, that method will be called constantly** by the OS without you having to deal with timers, loops etc.
** When the app is backgrounded the location updates will stop, and when the app comes back to the foreground the location updates will resume without you needing to restart them. These can come as often as once per second, but once again relies on the OS determining the location of the devices and delivering those updates to you.
If you're referring to anything else that is a different answer, but I suspect you're referring directly to location updates.
I suggest to put the function in another file, and use an NSThread
- (void)createThread
{
[NSThread detachNewThreadSelector:#selector(startBackgroundJob) toTarget:self withObject:nil];
}
- (void)startBackgroundJob
{
[self performSelectorOnMainThread:#selector(monitorApp) withObject:nil waitUntilDone:NO];
}
- (void)monitorApp
{
}