Function that fires after user's location has updated - ios

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.

Related

CLLocationManager and iBeacons: is requestState(for: region) necessary?

I'm trying to build an iBeacons app with Swift 4 in iOS 10. Many sources recommend the following:
func locationManager(_ manager: CLLocationManager, didStartMonitoringFor region: CLRegion) {
manager.requestState(for: region)
}
to handle the case when the app starts inside a beacon region. However, this approach seems to cause side effects and timing errors in some situations.
Now, I found this StackOverflow article Understanding iBeacons in iOS that states, "didDetermineState is only called automatically when you start monitoring".
Sure enough, if I leave out the call to requestState(for: region), and just call locationManager.startMonitoring(for: region)everything works perfectly!
Can anyone confirm that this is indeed the case? If I call startMonitoring then there is no need for requestState?
Note: I set notifyEntryStateOnDisplay to true before I start monitoring. I'm wondering if that has something to do with it.
Whether or not calling locationManager.requestState(for: region) is necessary all depends on your use case.
The callback locationManager(_ manager: CLLocationManager, didDetermineState state: CLRegionState, for region: CLRegion) will be called automatically in the following cases:
When you first start monitoring.
For all region state changes. This includes .outside -> .inside, .inside -> .outside as well as transitions to and from .unknown
Whenever the display is illuminated (only if you set your monitored BeaconRegion notifyEntryStateOnDisplay=true)
So if these cases are sufficient for you, then you don't need to request additional callbacks. This is generally true.
There are some rare use cases, however, where explicitly requesting a new callback is helpful. Perhaps your app presents a new View Controller and then wants to visually show the user the region state. It is convenient to be able to call requestState when the view loads, and then update the display when the callback is made.
Specific side effects and problems caused by extra callbacks really depend on what you put inside the callback. If you start or stop monitoring in these callbacks, it's very easy to create feedback loops that cause problems.

CLLocationManager Region Monitoring: Detect Arrival in Suspended State

I'm looking for a way that I can track that a user has arrived near a designated set of co-ordinates. The functionality needs to work while the application is in the background (preferably within 100 metres). Also, to preserve the battery, I ideally do not want to get too many co-ordinate readings (perhaps a reading every 10 minutes for no longer than a couple of hours).
There are a couple of ways that I have tried to accomplish this task, but have been unable to obtain the desired result:
Background Timer:
I had added a background task in (App.delegate)
func applicationDidEnterBackground(_ application: UIApplication)
Which executed a repeated Timer.scheduledTimer to get co-ordinates and process
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
to detect if the user was within range. This method worked if applied in the short-term, but only until the application was suspended, which was about 3 minutes. Ideally, I would not want to get co-ordinates this frequently.
Region Monitoring:
I had initialised the CLLocationManager as shown below:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
locationManager.delegate = self
locationManager.allowsBackgroundLocationUpdates = true
locationManager.pausesLocationUpdatesAutomatically = false
locationManager.activityType = .otherNavigation
locationManager.requestAlwaysAuthorization()
}
The LocationManager starts when the application enters into the background:
func applicationDidEnterBackground(_ application: UIApplication) {
self.monitorRegionAtLocation(center: CLLocationCoordinate2D(latitude: x, longitude: y), identifier: id)
locationManager.startUpdatingLocation()
}
Code for monitoring of region:
func monitorRegionAtLocation(center: CLLocationCoordinate2D, identifier: String ) {
// Make sure the app is authorized.
if CLLocationManager.authorizationStatus() == .authorizedAlways {
// Make sure region monitoring is supported.
if CLLocationManager.isMonitoringAvailable(for: CLCircularRegion.self) {
// Register the region.
let maxDistance = 200.0
let region = CLCircularRegion(center: center,
radius: maxDistance, identifier: identifier)
region.notifyOnEntry = true
region.notifyOnExit = false
locationManager.startMonitoring(for: region)
}
}
}
And I added a didEnterRegion function block for CLLocationManager:
func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
if let region = region as? CLCircularRegion {
let identifier = region.identifier
print("FOUND: " + identifier)
}
}
The code appears to work for detecting entry into a region, however the co-ordinates are not updating while in the background.
Additional Information
I have the Background Modes of Location Updates and Background Fetch enabled
I have supplied values for 'Location Always Usage Description' and 'Location When in Use Usage Description' in the Info.plist
The App Settings shows 'Always' permission against the Location
I believe that there has to be a better way of operating these kinds of checks in the background, but I haven't discovered any method of detecting other movements in the background.
Any direction on this matter would be greatly appreciated, and if you need any more information, please let me know and I'll provide what I can.
UPDATE:
I have modified the approach following the advice of comments below to use Region Monitoring.
Any location update/monitoring requires it's location manager to be configured properly so that it can work to the best to provide the desired location update. It's important to check some point when doing background location update:
1. Check background modes of location updates and background fetch should be enable
2. Check 'Location Always Usage Description' and 'Location When in Use Usage Description' in the Info.plist should be provided
3. Check if you want to pause in between location update - if yes then you need to provide activity type so that location manager can determine best way to pause location update for you
4. Check if you want to apply distance filter - you want user(device) to move some minimum amount for location manager to send updated location
5. Check if you want desired accuracy- This may cause power drain for certain accuracy type
In your code I can see location manager is configured with some of the parameter but missing accuracy and distance filter for background mode.
locationManager.allowsBackgroundLocationUpdates = true
locationManager.pausesLocationUpdatesAutomatically = false
locationManager.activityType = .otherNavigation
Also, if you see pause location update property in Apple doc it says:
For apps that have in-use authorization, a pause to location updates
ends access to location changes until the app is launched again and
able to restart those updates. If you do not wish location updates to
stop entirely, consider disabling this property and changing location
accuracy to kCLLocationAccuracyThreeKilometers when your app moves to
the background. Doing so allows you to continue receiving location
updates in a power-friendly manner.
Essentially it tells that if you want disable pause then you have to keep accuracy level (kCLLocationAccuracyThreeKilometers). Which I guess is missing in your approach.
Also, you can check this link which actually starts background task and then starts location manager monitoring inside the background task.
Hope it helps.
The question says "CLLocationManager Region Monitoring: Detect Arrival in Background". And this is very much possible, but detecting anything after being killed is not possible (from iOS 7).
Whenever user swipe ups your app app-switcher, iOS takes it as the user doesn't wish the app to be running in the background, and so all the call-backs are stopped.
This answer, this answer and this answer also says the same thing. However Apple doc is a little confusing.
My personal observation is that app gets called even in Killed mode but very rarely.
And about getting the location, whenever the delegate method of geofencing is called, you can get location easily.
And the background modes are really not needed for your requirement.
And unfortunately (fortunately for iOS user as they save battery) we don't really have a way to get location just for 1 hr after app being killed.

Run action when iOS device is near some map point

I have the mobile app that uses user's location when the app is running to show some places on a map (now it is Google Map).
I want to add a new feature - App in the background should notify the user when the device is near some map point or entering the predefined area.
How can it be done? Are there different approaches?
Where to start from?
What if my points stored on the server - should I save them on the device first and what if can't do it and new map points can be added when the app is not in memory?
What will be the most challenging in creating such feature?
p.s. I know about Ray's geofencing tutorial but I want to know - are there any different approaches and also about pitfalls of such feature
First of all you must enable location updates and remote notifications in background modes(Your Target->Capabilities), then you must write in your location update function some code to detect that user is close to some location exapmle:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let userLocation = locations.first {
if location.coordinate.langtitude <= <some value> ||
location.coordinate.langtitude >= <some value> || ... {
runAction()
}
}
}
func runAction() {
//show local notification here
}

Completion block for GPS coordinates

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.

Which one to use "locations array" or "manager.location.coordinate" in Swift in iOS to get user location?

I want to get the user's location. It might be approximate position, that's fine.
In didUpdateLocations method, i saw two ways to get the coordinates.
Using manager.location.coordinate
Using locations array
Which one should I go for? I am thinking locations array will contain user's recent location as I am starting startUpdatingLocation() as soon as user opens the app.
So which one should i use to get the coordinates? And what does manager.location.coordinate do in this case?
Note: Accuracy of location need not be accurate to 10 meters. Rough estimation is enough.
Here is my code:
func initLocationUpdates() {
if CLLocationManager.locationServicesEnabled() {
print("going to get location")
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
locationManager.startUpdatingLocation()
}
else {
print("location services disabled")
}
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let locValue:CLLocationCoordinate2D = manager.location!.coordinate
print("locations = \(locValue.latitude) \(locValue.longitude)")
/*
let userLocation:CLLocation = locations[0] // or maybe even locations.last
let long = userLocation.coordinate.longitude;
let lat = userLocation.coordinate.latitude;
print("lat: ", lat, " --- long: ", long)
*/
locationManager.stopUpdatingLocation()
}
I'd prefer the array over the instance variable, because of the way desiredAccuracy works - it doesn't guarantee that the location updates you receive will have the accuracy you request - merely that the system will make its best effort to provide them. Depending on how the location manager behaves, you might get a number of updates in the locations array, with differing levels of accuracy. You can then filter the array to find the most accurate one.
The key point is that if multiple updates arrive in the same didUpdateLocations: call, and you just use manager.location, you might be missing an update. The missing update would definitely not be the most recent one, but it might be the most accurate one received so far.
Since you have set desiredAccuracy to a very high level (ten meters), I'm guessing precision is more important to you than timing - it will take a while for the system to deliver an update of that level of accuracy (and it may never arrive if you are indoors or otherwise blocked from using GPS). So the likelihood of the scenario above occurring is reasonable.
As for the purpose of the instance variable location, the docs suggest that it's intended to be used in certain circumstances when the app restarts:
In iOS 4.0 and later, this property may contain a more recent location object at launch time. Specifically, if significant location updates are running and your app is terminated, this property is updated with the most recent location data when your app is relaunched (and you create a new location manager object). This location data may be more recent than the last location event processed by your app.

Resources