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

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.

Related

Function that fires after user's location has updated

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.

Perform task (code execution/ network call) in background when user enters/exits a location region in iOS

System: In my application, I am using geofencing (monitoring a region). Whenever user enters or exits the monitored area or region, the app shows a local notification if the app is in the background or even terminated. This is working perfectly fine. The app is able to show local notificaiton.
Now I also need to submit this information (if the user is inside or outside of the monitored area) via HTTP POST call to app's backend server.
Problem: App makes API call in LocationManager's delegate methods but sometimes it works and sometimes it does not. It seems that code execution stops randomly if the app is not in foreground state.
Code sample
// MARK: - Location Manager Delegate
func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
if region is CLCircularRegion {
showLocalNotification(forRegion: region)// Works
updateUserEntryAPICall(region: region) // Sometimes works
}
}
func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
if region is CLCircularRegion {
showLocalNotification(forRegion: region) // Works
updateUserExitAPICall(region: region) // sometime works
}
}
I did not turn on background mode capability in iOS. Still, the app is able to show local notifications. Do I need to turn it on in order to make network call to work?
Please help.
This looks like you're using a regular URLSession to make your API request. You need to make sure that you're doing this on a session that handles running in the background, e.g. by initializing it like so:
let session = URLSession(configuration: .background(withIdentifier: "foo"))

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.

How to avoid gaps in gps tracking on iOS 11

I am experiencing a problem in a tracking app, but only on iOS 11. The app passively records your GPS position in the background under certain conditions.
The problem that occurs on iOS 11 is that seemingly randomly CLLocationManager stops reporting GPS events for anywhere from 10 to 900+ seconds.
The location manager is set up like the following:
let locationManager = CLLocationManager()
locationManager.allowsBackgroundLocationUpdates = true
locationManager.pausesLocationUpdatesAutomatically = false
locationManager.startMonitoringSignificantLocationChanges()
locationManager.desiredAccuracy = 10
locationManager.activityType = .automotiveNavigation
Thinking that the thread CoreLocation is managing and using for all callbacks could be overburden.
I have tried delegating to a different thread for processing, so the app does not tie up CoreLocations resources. This is done using an operation queue that is set up like the following:
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
queue.qualityOfService = .userInitiated
with the callback using that operation queue:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
queue.addOperation {
// process locations
}
}
Introducing the operation queue did not help avoid the gaps, but it did make it so that when the gap occurs the location manager reports a bunch of (different) locations with the same timestamp.
The locations with the same timestamps is not all the missing locations, i.e. if there is a gap of 200 seconds, I might only get 15 locations with the same timestamp.
I am hoping someone here can tell me why this is happening and what I can do to avoid these gaps.
Thanks in advance.
After much review, trials, discussion with Apple we now seem have resolve the issue. Even though Apple will only guarantee GPS tracking the background if the app is in the foreground when the tracking starts. The fix we have applied is changing the following:
locationManager.startMonitoringSignificantLocationChanges()
... to:
locationManager.stopMonitoringSignificantLocationChanges()
locationManager.startMonitoringSignificantLocationChanges()
The theory is that the settings for the app become corrupted when applying true to the "monitoring" setting during startup. If first you set the "monitoring" setting to false and afterwards to true, no corruption occurs.
This fix is live in the app store app for several hundreds of people and tracking in the background is working fine.

How can I get trueHeading just once in iOS?

As a fresh iOS developer, I am confused to get trueHeading data recently.
class AR: CLLocationManagerDelegate{
var heading:Float!
func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
heading = Float(newHeading.trueHeading)
}
}
Via above code, I can continuously get current device's direction. However, can I just get the trueHeading just once? In addition, I tested the trueHeading, it will be accurate after seconds. Can I get the trueHeading at a particular time?
Thank you for any help!
Whenever you need heading you can call
[self.locationManager startUpdatingHeading];
Once you have received the heading, you can do a
[self.locationManager stopUpdatingHeading];
To receive the latest derived heading,
self.locationManager.heading;
So ideally, you call startUpdatingHeading, show a loader or something to the user.
Once you get a heading in didUpdateHeading, relay a message to the controller via a notification or something, and stopupdating heading.
You cannot set the accuracy of the Heading, but you can set the headingFilter to only update when there is a large distance.
/* Notify heading changes when heading is > 5.
* Default value is kCLHeadingFilterNone: all movements are reported.
*/
self.locationManager.headingFilter = 5;
Ideally, you do not stop ever, you continue accessing the heading, with a filter of 5, ensuring it does not drain battery.
You store this in the defaults lets say, and use it whenever needed and update it.
If you need it just for once, then you may do a quick fix by waiting for some duration or lets say some hits to the function, but that is not an ideal solution.
Maybe this can also help, but it is primarily for distance, and not heading. Heading is supposed to be constantly updated.
self.locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters;
If you want this on app launch, do it in the app delegate.

Resources