I'm working on an application that should be able to collect location data while the application is in background.
To allow this, I have done the following:
In the project settings, under the Capabilities tab I have ticked Location updates.
I have set the NSLocationAlwaysUsageDescription key in the Applications Info.plist and have assigned an appropriate string value.
I have configured an instance of CLLocationManager like so:
locationManager.activityType = .Fitness
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.pausesLocationUpdatesAutomatically = false
Before beginning location collection I call locationManager.requestAlwaysAuthorization(), I have the appropriate delegate callbacks to handle any error.
lastly, I call locationManager.startUpdatingLocation()
When running the application on the simulator, I get a print out of the location updates, and the application's badge is also updated with the number of locations collected, but when running the applications on an actual device, after sending the app in the background, location updates are shortly stopped from being handled by the app (I know this as the badge stops updating).
Am I missing some sort of device specific configuration?
Here is my setting that works all right when the app is in the background or the device in standby mode:
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = kCLDistanceFilterNone
locationManager.pausesLocationUpdatesAutomatically = false
locationManager.activityType = .Other
locationManager.startUpdatingLocation()
I am also using deferred location updates. In didUpdateLocations:
if CLLocationManager.deferredLocationUpdatesAvailable() && deferringUpdates == false {
locationManager.allowDeferredLocationUpdatesUntilTraveled(CLLocationDistanceMax, timeout: deferredLocationTimeout)
deferringUpdates = true
}
and in didFinishDeferredUpdatesWithError:
deferringUpdates = false
Setting the capabilities is not enough as of iOS 9. You need to also set locationManager.allowsBackgroundLocationUpdates = true on your Location Manager before OS can deliver updates to you in the background. It is compulsory to do so according to the documentation. By default it is set to false. See here.
Related
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)
}
}
}
So I wrote a stat tracker for skiing / snowboarding that was working just fine last season. This season I'm having the hardest time keeping the app alive even though I'm setup to receive background location updates.
Here are some of the tasks that are running in the background on timers:
-Parsing arrays / dictionaries and performing calculations
-Receiving and sending data to firebase
-Recording data to CoreData
As long I have cell signal things have a better chance of staying alive but no guarantee.
Did something change in iOS 12 vs 11?
Here's my code for starting location updates:
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.activityType = .other
locationManager.allowsBackgroundLocationUpdates = true
locationManager.pausesLocationUpdatesAutomatically = false
locationManager.showsBackgroundLocationIndicator = true
locationManager.startUpdatingLocation()
I've tried locationManagerDidPauseLocationUpdates to restart location updates if iOS tried to kill it, changing the activity type, changing the accuracy, nothing seems to work although changing the activity type to other did extend the amount of time before the app dies so I thought locationManagerDidPauseLocationUpdates would restart it but no dice.
Any other ideas on keeping things alive while recording?
edit: I've also tried checking for memory leaks
edit2: added screenshot of info.plist
So I don't know why but after some experimentation and moving things around I seem to have fixed it.
I had CllocationManager as a singleton and moved it to a struct as I'm sharing it between multiple VCs as to not instantiate it more than once.
I then moved
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.activityType = .other
locationManager.allowsBackgroundLocationUpdates = true
locationManager.pausesLocationUpdatesAutomatically = false
locationManager.showsBackgroundLocationIndicator = true
out of its own function and into viewDidLoad() which still didn't work so I moved it to viewDidAppear() and now my app has been running for 8 hours without a hiccup. I'm not sure if the way I was sharing the location manager before was causing a second instantiation and a reset of the location manager settings to default which would cause locationManager.pausesLocationUpdatesAutomatically to become true (not sure just guessing)
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.
I would like to know whether the following is implementable ?
I want my iOS app to give users a notification when the reach a particular location as set in the app (even if the app is killed)
You can achieve this with the new User Notification framework introduced in iOS 10.
There have a UNLocationNotificationTrigger which let you specify a CLRegion. A notification will be post when the user’s device enters or leaves the CLRegion. Use the CLRegion object to specify whether to deliver notifications on entry, on exit, or both.
Refer to:
UserNotifications framework:
https://developer.apple.com/reference/usernotifications
UNLocationNotificationTrigger:
https://developer.apple.com/reference/usernotifications/unlocationnotificationtrigger
When the app is killed you can't , but you can still do it in background mode.
First Way:
UNLocationNotificationTrigger notifies user when user enters the specified location or exit the specified location this link provides exact implementation.
Second Way:
monitoring significant changes can do this for you but the result may not be 100%.
call following method from applicationDidEnterBackground and applicationWillTerminate:;
- (void)startSignificantChangeUpdates
{
// Create the location manager if this object does not
// already have one.
if (nil == locationManager)
locationManager = [[CLLocationManager alloc] init];
locationManager.delegate = self;
[locationManager startMonitoringSignificantLocationChanges];
}
and when app enters foreground applicationWillEnterForeground: call following method (modify it as per your requirements i.e. accuracy & distance filter)
- (void)startStandardUpdates
{
// Create the location manager if this object does not
// already have one.
if (nil == locationManager)
locationManager = [[CLLocationManager alloc] init];
locationManager.delegate = self;
locationManager.desiredAccuracy = kCLLocationAccuracyKilometer;
// Set a movement threshold for new events.
locationManager.distanceFilter = 500; // meters
[locationManager startUpdatingLocation];
}
From apple documentation:
The significant-change location service delivers updates only when there has been a significant change in the device’s location, such as 500 meters or more. It’s crucial that you use the significant-change location service correctly, because it wakes the system and your app at least every 15 minutes, even if no location changes have occurred, and it runs continuously until you stop it.
For 100% accuracy you can involve server end and send silent notifications from server which will give your application a 30 second window to determine user location and fire a local notification.
I have an app that makes a call to get the user's location:
-(void)getLocation{
locationManager = [[CLLocationManager alloc] init];
locationManager.delegate = self;
locationManager.distanceFilter = kCLDistanceFilterNone;
locationManager.desiredAccuracy = kCLLocationAccuracyBest;
[locationManager startUpdatingLocation];
}
//SET USER LOCATION
-(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations {
self.userLocation = [locations lastObject];
NSLog(#"location in IntroVC %f, %f", self.userLocation.coordinate.latitude, self.userLocation.coordinate.longitude);
}
My question is, because that NSLog keeps spitting out a new location infinitely, when should I stop calling the location? Well I guess its really up to my app's functionality, but doesnt this cause battery drain? If so, I should really look into the best way of stopping the updates.
Your Distance filter of the location manager is set to be kCLDistanceFilterNone. This causes the didUpdateLocations method to be called infinite time.
locationManager.distanceFilter = kCLDistanceFilterNone;
Change this line as
locationManager.distanceFilter = 10;
and try again. Change the value as needed.
So now the didUpdateLocation will not be called infinite times. :)
Hopefully this helps.
Depending on the nature of your app, you may well want to turn location services off when you go into the background or when the screen is locked. These notifications hooks are provided generally in your app delegate file (.m). Yes you are right, location services significantly drain battery and it a highly recommended practice for iOS apps using location services to use it cautiously.
Apple seems to have thought about this and has provided an API that notifies the app only if the user has moved "significantly" Apple the significant change location service. The definition of "moved significantly" varies on various aspects depending on WiFi availability, cell tower availability, GPS availability etc. Luckily all this is obfuscated within this API.
I call this method after updating the location in didupdateloactions, so when my app goes in background it remove location icon from top status bar.