CLLocationManager Region Monitoring: Detect Arrival in Suspended State - ios

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.

Related

Starting locations updates after enter beacon region monitored Swift

I'm making a ibeacon region monitoring app with location updates when the user enter into this region (app not in foreground). This location updates must be configured as kCLLocationAccuracyBestForNavigation accuracy (because I have to make a tracking while the use remain in the region,
subscribe to me significant changes is not enough). Everything works well, but after 20 seconds (sometimes 1 minute o more) I stop receiving locations updates. I put all the keys in info.plist for always location usage, I include the background modes in capabilities section and locations updates on background.
I configure the locationManager with different configurations and always the SO stops my locations updates. I'm using IOS 12 and Iphone 7 for testing.
The way I configure CLLocationManager:
self.locationManager.desiredAccuracy
=kCLLocationAccuracyBestForNavigation
self.locationManager.activityType = .automotiveNavigation
self.locationManager.distanceFilter = kCLDistanceFilterNone;
self.locationManager.delegate = self
self.locationManager.allowsBackgroundLocationUpdates = true
self.locationManager.pausesLocationUpdatesAutomatically = false
Start location updates (when user enter in Ibeacon Region):
func beaconManager(_ manager: KTKBeaconManager, didEnter region:
KTKBeaconRegion) {
self.locationManager.startUpdatingLocation()
}
And finally, in didUpdate locations i call a web service:
func locationManager(_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]) {
//Call web service using alamofire
}
I ask for your help to know if I am performing the settings correctly for the purpose I want to perform and any clue that lets me know why the operating system kills my process to get locations updates
Getting regular location updates in the background on iOS is tricky. The operating system is designed to keep apps from constantly running in the background to optimize battery usage, and it suspends them after a period of time unless you have several things exactly right.
You need to do three things:
You must get obtain always location permission from the user (as you say you've done).
You must add the following entry to your Info.plist. This will allow your app to run indefinitely in the background, however if you wish to submit your app to the App Store, this entry will also declare to reviewers that it is a background location app, and you will need to convince them that it provides a location-based benefit to the user, and that the user is aware of this benefit.
<key>UIBackgroundModes</key>
<array>
<string>location</string>
</array>
You must maintain a background thread to keep your app alive. It doesn't matter if you actually do anything in this background thread. Just having it be active keeps iOS from suspending your app.
var backgroundTask: UIBackgroundTaskIdentifier = UIBackgroundTaskIdentifier.invalid
func extendBackgroundRunningTime() {
if (backgroundTask != UIBackgroundTaskIdentifier.invalid) {
// already started
return
}
NSLog("Attempting to extend background running time")
self.backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "DummyTask", expirationHandler: {
NSLog("Background task expired by iOS. Cannot run again until a new beacon region event")
UIApplication.shared.endBackgroundTask(self.backgroundTask)
self.backgroundTask = UIBackgroundTaskIdentifier.invalid
})
DispatchQueue.global().async {
while (true) {
let backgroundTimeRemaining = UIApplication.shared.backgroundTimeRemaining
// This will be a very large number if you have proper permissions
// If not, it will generally count down from 10 seconds once you are in the
// background until iOS suspends your app.
NSLog("Thread background time remaining: \(backgroundTimeRemaining)")
Thread.sleep(forTimeInterval: 1.0)
}
}
}

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"))

How to make intermitent HTTP POST requests in the background of an iOS app - Swift

Hi I'm making an app that sends POST location update requests to a web application. I have one function called locationUpdate that I'd like to call intermittently once the app is in the background however I have't found a way to do it yet.
I have all the basics like Allow Location Always set. The app currently makes one HTTP POST request right when it enters the background because of this code in the AppDelegate file:
func applicationDidEnterBackground(_ application: UIApplication) {
sendLocation.determineCurrentLocation()
}
I also have this in the AppDelegate because I've seen it in a few stack overflow answers:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplicationBackgroundFetchIntervalMinimum)
return true
}
So all this but the app doesn't call sendLocation.determineCurrentLocation() (The function that makes the POST request) more than once and that is when the app is closed.
Can anyone tell me how to implement intermittent calling of the sendLocation function?
EDITED to show my locationManager:
func determineCurrentLocation(){
locationManager.delegate = self
locationManager.allowsBackgroundLocationUpdates=true
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestAlwaysAuthorization()
if CLLocationManager.locationServicesEnabled(){
DispatchQueue.main.async {
self.locationManager.requestLocation()
}
}
else if (!CLLocationManager.locationServicesEnabled())
{
print("You need to enable location services to use this app")
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]){
locationManager.stopUpdatingLocation()
userLocation = locations[0] as CLLocation
locationStruct.latitude=userLocation?.coordinate.latitude
locationStruct.longitude=userLocation?.coordinate.longitude
print(userLocation?.coordinate.latitude)
print(userLocation?.coordinate.longitude)
sendLocationPost()
}
You won't be able to do what you are trying to do unless you set up your app as a turn-by-turn navigation app and ask for permission to get location updates in the background.
Apps only get a very short time in the background before they are suspended.
You can ask for more background time with the beginBackgroundTask(expirationHandler:) call, but that's normally limited to 3 minutes of background time at max. (Search on that method to find the docs about it.)
Only certain types of apps are allowed to run indefinitely in the background. (Turn-by-turn navigation apps and streaming music apps.) You have to set up your info.plist to declare that your app is one of those types of apps. That is documented in the iOS docs that come with Xcode.
EDIT:
As Paul points out in his comment, you CAN ask for "always" location update permission and then ask for significant location change updates, although those are rather crude and you can't control the time interval between location updates (and won't get any updates until the OS decides the user has moved a significant distance.)

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.

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
}

Resources