Trying to get my app to use less power, it's tracking location always in the background but I'd like for it automatically pause so I can turn on region watching and use that to resume precise location monitoring once the user moves around a bit.
I've had the app on for half an hour now and the location service is not pausing. I think this has been the case since Apple changed location stuff in iOS 13? I'm not really sure. All the documentation I can find online seems extremely outdated.
Any advice is greatly appreciated, relevant code follows:
init() {
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = 10
locationManager.activityType = .fitness
locationManager.requestAlwaysAuthorization()
locationManager.allowsBackgroundLocationUpdates = true
locationManager.pausesLocationUpdatesAutomatically = true
locationManager.showsBackgroundLocationIndicator = true
locationManager.startUpdatingLocation()
}
func locationManagerDidPauseLocationUpdates(_ manager: CLLocationManager) {
delegate?.paused(tracker: self)
print("MT | LOCATION SERVICES PAUSED!") <---- NEVER GETTING CALLED (been running for 40 minutes now, no location updates, still going though?)
// if not already, start region monitoring
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// Collect data
for location in locations {
print("MT | New Location: \(location)")
}
}
I wouldn't rely on connecting to Xcode. Xcode is generally greedy. It doesn't want you to stop your debugging. So it never puts the app in a suspended state.
The callback of locationManagerDidPauseLocationUpdates is somewhat identical to your app being suspended which Xcode prevents.
The way I would test this is to use os.log. Then log the outputs to your Mac's console app. See here.
Just make sure the app is not launched from you running from Xcode. I'd rather disconnect your device from Xcode, kill the app. Disconnect your device from Xcode. Then just tap on the app icon again. That way Xcode cannot intervene.
The issue is caused by your desired accuracy.
You are setting the accuracy to the best, which will make your application run continuously, so it will never pause in any iOS versions.
Furthermore, in a practical test whether it is pausing or not, change the accuracy to kCLLocationAccuracyHundredMeters. It will definitely pause. You can change the filter accuracy as you need for the next update.
Do you think maybe your activityType is preventing the location services to pause?
activityType
The location manager uses the information in this property as a cue to determine when location updates may be automatically paused. Pausing updates gives the system the opportunity to save power in situations where the user's location is not likely to be changing.
.fitness
This activity might cause location updates to be paused only when the user does not move a significant distance over a period of time.
I don't know what your particular use case is but, have you considered using the following function for background location tracking?
startMonitoringSignificantLocationChanges()
You may also try a mix of both, turning on significant location changes for day to day use and allowsBackgroundLocation when doing something like tracking a run, etc.
I think it is quite clear from the docs that this the pause functionality does not work like that. First of all, there is no guarantee that the GPS hardware will pause at all, it depends on the type of activity as well as other activities in the system. It is up to iOS to decide when and if to pause location updates, the property is just a hint.
More important, your use case would not work since if it gets paused, the user would have to interact with your app manually to resume updates.
Apple mentions your case specifically and recommends reducing the accuracy of location updates so it is only using cell tower triangulation instead of GPS (see docs above).
So, instead of using pausesLocationUpdatesAutomatically = true, you could do desiredAccuracy = kCLLocationAccuracyThreeKilometers.
To quote Apple,
[...] 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
Doesn't this cover your use case?
Related
I'm working on a navigation application, everything working in terminated, background and fore ground state.
But in one scenario of terminated state startMonitoringSignificantLocationChanges is not handling itself.
The issue is
when i start the startMonitoringSignificantLocationChanges and killed the app, then I'm getting location event like after 0.5-1km because of that it draws straight line from my initial position to the first location event I get.But when the location event starts coming then everything work smoothly
Same issue occur again when in the middle of travelling I open the application to check my route status and then kill the application, again location events start coming after 0.5-1km and a straight line was drawn.
The code is straight
significantLocationManager = CLLocationManager()
significantLocationManager?.allowsBackgroundLocationUpdates = true
significantLocationManager?.pausesLocationUpdatesAutomatically = false
significantLocationManager?.requestAlwaysAuthorization()
and call the tracking when user needs by
significantLocationManager?.startMonitoringSignificantLocationChanges()
Rest I have handled the incoming location event in the app delegate to save in db.
So question is how should I handle this scenario in which straight line is drawn ?
From Apple documentation:
Apps can expect a notification as soon as the device moves 500 meters or more from its previous notification. It should not expect notifications more frequently than once every five minutes. If the device is able to retrieve data from the network, the location manager is much more likely to deliver notifications in a timely manner.
If you need to receive location updates as soon as possible I'd recommend to use startUpdatingLocation() with desired distanceFilter of CLLocationManager.
You can use Location update in background mode. From Apple documentation:
When you start the significant-change location service, a recently
cached value may be reported to your delegate immediately. As new
location data is obtained, the location manager calls your delegate's
locationManager(_:didUpdateLocations:) method with the updated values.
The locations parameter always contains at least one location and may
contain more than one. Locations are always reported in the order in
which they were determined, so the most recent location is always the
last item in the array, as shown in Listing 2.
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let lastLocation = locations.last!
// Do something with the location.
}
Here you will get the last cached location in your device, and it should be very precise if you have location service turned on in your device of course.
Another thing to know is this. Note form Apple:
The significant-change location service requires authorization. For
more information Requesting Authorization for Location Services.
I've created a simple app which tracks user location and creates local notification for every time location is updated.
I enabled the background modes below,
let locationManager = CLLocationManager()
open override func viewDidLoad() {
locationManager.delegate = self;
locationManager.desiredAccuracy = kCLLocationAccuracyBest;
locationManager.distanceFilter = 10
locationManager.allowsBackgroundLocationUpdates = true
locationManager.startUpdatingLocation()
}
open func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let notification = UILocalNotification()
notification.alertBody = "location updated"
notification.fireDate = Date()
UIApplication.shared.scheduleLocalNotification(notification)
}
I set string for NSLocationAlwaysUsageDescription and ask for permission. User grant permission for always usage when the app loaded first time.
It's working well when app is in the foreground, when it goes background still working at least in 5-40 minutes time range which is changeable
by battery or other opened apps.
The problem is why it stops working, Isn't it expected to be keep working?
I've never seen a time limit in Apple docs.
Switch to significant location updates when the app moves to background. iOS will unload the app if it keep alive in the background indefinitely.
locationManager.pausesLocationUpdatesAutomatically = false
Reduce Accuracy
Set the desiredAccuracy property of the location manager object
self.locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
You can use one of the CLLocationAccuracy constants
IMPORTANT
By default, standard location updates on iOS devices run with an accuracy level of best. Change these settings to match
your app’s requirements. Otherwise, your app will unnecessarily waste
energy.
Auto-Pause
Set the pausesLocationUpdatesAutomatically property of the location manager object to true
self.locationManager.pausesLocationUpdatesAutomatically = true
IMPORTANT
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.
Allow background updates
Set the allowsBackgroundLocationUpdates property of the location manager object to true
self.locationManager.allowsBackgroundLocationUpdates = true
Apps that want to receive location updates when suspended must include
the UIBackgroundModes key (with the location value) in their app’s
Info.plist file and set the value of this property to true. The
presence of the UIBackgroundModes key with the location value is
required for background updates
Specify an Activity Type
Set the activityType property to let Core Location know what type of location activity your app is performing at a given time
self.locationManager.activityType = .automotiveNavigation
You can use one of the CLActivityType cases
Defer Location Update
On supported devices with GPS hardware, you can let the location
manager defer the delivery of location updates when your app is in the
background. For example, a fitness app that tracks the user’s location
on a hiking trail can defer updates until the user has moved a certain
distance or a certain period of time has elapsed.
Energy Efficiency Guide for iOS Apps - Location Best Practices
GettingLocationWhenSuspended
After searching for references (talking about any limitation), I assume that Apple Core Location Best Practices video session could be useful! at 06:53 talking about standard location in the background:
furthermore, Core Location won't take any action to ensure your app
continues to run, so if you have background run for some reason and
you decide to start a location session you might get some updates, but
you might also get suspended before you receive all information that
you hope to receive...
Actually, I faced this issue before, -as a workaround- the core location was used to keep tracking the location of the user to do unrelated functionality to its location -which is uploading files-, but this workaround didn't work since iOS 9 has been released; I even posted a question referring to this issue.
However, it seems your case is not identical to what I faced, if you are aiming to:
... creates local notification for every time location is updated.
then you might need to follow the approach of integrating with User Notification Framework - UNLocationNotificationTrigger:
The geographic location that the user must reach to enable the
delivery of a local notification.
It is also mentioned in the video session (08:59).
Probably, this is could be not what are you looking for, but since we have no guarantee that the background execution will continue running, you might -somehow- find a way to integrate it in your app to achieve the desired functionality.
Update for iOS 11:
You might need to check this answer for the proper way to request the location access.
By the sound of it the app is being killed due to memory constraints.
It should however be re-launched when a new location becomes available, as described here: https://developer.apple.com/documentation/uikit/uiapplicationlaunchoptionskey/1623101-location
You should see application(_:didFinishLaunchingWithOptions:) being called, and the 'location' key should be present in the launch options. You'll can then re-create whatever is consuming the locations and continue recording.
If it's not being re-launched it could be too memory hungry. Check the memory consumption of the app and see if applicationDidReceiveMemoryWarning(_:) is being called.
I assume you haven't implement background task. You can read here.
In the above link under section "Implementing Long-Running Tasks" point no. 3 is your situation, so it's valid you can use background location update in your project and for same you need to implement a background task too.
There are three way to track user location(as per above link under section "Tracking the User’s Location" ) :-
Foreground-only location services (which works in your case)
The significant-change location service (Recommended), but I think it is not usable in your case as you want to update user location per 10 meter and it works for ~500 meters, for more please see here
Background location services (I think you are trying for this) and solution is to add a background task.
Below is example of background task and you can modify as per your requirement, it works for me since last 2 hours and my app still update location in background.
.
In your AppDelegate class please update below function and then run your app in background.
func applicationDidEnterBackground(_ application: UIApplication) {
application.beginBackgroundTask(withName: "") {}
}
And below is my ViewController class
import UIKit
import CoreLocation
class ViewController: UIViewController, CLLocationManagerDelegate {
let locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
locationManager.delegate = self;
locationManager.requestAlwaysAuthorization()
locationManager.desiredAccuracy = kCLLocationAccuracyBest;
locationManager.allowsBackgroundLocationUpdates = true
locationManager.distanceFilter = kCLDistanceFilterNone
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { (timer) in
self.locationManager.startUpdatingLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print("didUpdateLocations \(Date())")
self.locationManager.stopUpdatingLocation()
}
}
I ran into this where a QT app would stop getting location events in the background after an hour or two.
In the App Delegate when the app would go into the background I would stop the CLLocationManager, decrease the Accuracy from kCLLocationAccuracyBest to kCLLocationAccuracyNearestTenMeters AND increase the distance filter from None to 50 meters and it would then track for 24+ hours.
I've created a simple app which tracks user location and creates local notification for every time location is updated.
I enabled the background modes below,
let locationManager = CLLocationManager()
open override func viewDidLoad() {
locationManager.delegate = self;
locationManager.desiredAccuracy = kCLLocationAccuracyBest;
locationManager.distanceFilter = 10
locationManager.allowsBackgroundLocationUpdates = true
locationManager.startUpdatingLocation()
}
open func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let notification = UILocalNotification()
notification.alertBody = "location updated"
notification.fireDate = Date()
UIApplication.shared.scheduleLocalNotification(notification)
}
I set string for NSLocationAlwaysUsageDescription and ask for permission. User grant permission for always usage when the app loaded first time.
It's working well when app is in the foreground, when it goes background still working at least in 5-40 minutes time range which is changeable
by battery or other opened apps.
The problem is why it stops working, Isn't it expected to be keep working?
I've never seen a time limit in Apple docs.
Switch to significant location updates when the app moves to background. iOS will unload the app if it keep alive in the background indefinitely.
locationManager.pausesLocationUpdatesAutomatically = false
Reduce Accuracy
Set the desiredAccuracy property of the location manager object
self.locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
You can use one of the CLLocationAccuracy constants
IMPORTANT
By default, standard location updates on iOS devices run with an accuracy level of best. Change these settings to match
your app’s requirements. Otherwise, your app will unnecessarily waste
energy.
Auto-Pause
Set the pausesLocationUpdatesAutomatically property of the location manager object to true
self.locationManager.pausesLocationUpdatesAutomatically = true
IMPORTANT
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.
Allow background updates
Set the allowsBackgroundLocationUpdates property of the location manager object to true
self.locationManager.allowsBackgroundLocationUpdates = true
Apps that want to receive location updates when suspended must include
the UIBackgroundModes key (with the location value) in their app’s
Info.plist file and set the value of this property to true. The
presence of the UIBackgroundModes key with the location value is
required for background updates
Specify an Activity Type
Set the activityType property to let Core Location know what type of location activity your app is performing at a given time
self.locationManager.activityType = .automotiveNavigation
You can use one of the CLActivityType cases
Defer Location Update
On supported devices with GPS hardware, you can let the location
manager defer the delivery of location updates when your app is in the
background. For example, a fitness app that tracks the user’s location
on a hiking trail can defer updates until the user has moved a certain
distance or a certain period of time has elapsed.
Energy Efficiency Guide for iOS Apps - Location Best Practices
GettingLocationWhenSuspended
After searching for references (talking about any limitation), I assume that Apple Core Location Best Practices video session could be useful! at 06:53 talking about standard location in the background:
furthermore, Core Location won't take any action to ensure your app
continues to run, so if you have background run for some reason and
you decide to start a location session you might get some updates, but
you might also get suspended before you receive all information that
you hope to receive...
Actually, I faced this issue before, -as a workaround- the core location was used to keep tracking the location of the user to do unrelated functionality to its location -which is uploading files-, but this workaround didn't work since iOS 9 has been released; I even posted a question referring to this issue.
However, it seems your case is not identical to what I faced, if you are aiming to:
... creates local notification for every time location is updated.
then you might need to follow the approach of integrating with User Notification Framework - UNLocationNotificationTrigger:
The geographic location that the user must reach to enable the
delivery of a local notification.
It is also mentioned in the video session (08:59).
Probably, this is could be not what are you looking for, but since we have no guarantee that the background execution will continue running, you might -somehow- find a way to integrate it in your app to achieve the desired functionality.
Update for iOS 11:
You might need to check this answer for the proper way to request the location access.
By the sound of it the app is being killed due to memory constraints.
It should however be re-launched when a new location becomes available, as described here: https://developer.apple.com/documentation/uikit/uiapplicationlaunchoptionskey/1623101-location
You should see application(_:didFinishLaunchingWithOptions:) being called, and the 'location' key should be present in the launch options. You'll can then re-create whatever is consuming the locations and continue recording.
If it's not being re-launched it could be too memory hungry. Check the memory consumption of the app and see if applicationDidReceiveMemoryWarning(_:) is being called.
I assume you haven't implement background task. You can read here.
In the above link under section "Implementing Long-Running Tasks" point no. 3 is your situation, so it's valid you can use background location update in your project and for same you need to implement a background task too.
There are three way to track user location(as per above link under section "Tracking the User’s Location" ) :-
Foreground-only location services (which works in your case)
The significant-change location service (Recommended), but I think it is not usable in your case as you want to update user location per 10 meter and it works for ~500 meters, for more please see here
Background location services (I think you are trying for this) and solution is to add a background task.
Below is example of background task and you can modify as per your requirement, it works for me since last 2 hours and my app still update location in background.
.
In your AppDelegate class please update below function and then run your app in background.
func applicationDidEnterBackground(_ application: UIApplication) {
application.beginBackgroundTask(withName: "") {}
}
And below is my ViewController class
import UIKit
import CoreLocation
class ViewController: UIViewController, CLLocationManagerDelegate {
let locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
locationManager.delegate = self;
locationManager.requestAlwaysAuthorization()
locationManager.desiredAccuracy = kCLLocationAccuracyBest;
locationManager.allowsBackgroundLocationUpdates = true
locationManager.distanceFilter = kCLDistanceFilterNone
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { (timer) in
self.locationManager.startUpdatingLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print("didUpdateLocations \(Date())")
self.locationManager.stopUpdatingLocation()
}
}
I ran into this where a QT app would stop getting location events in the background after an hour or two.
In the App Delegate when the app would go into the background I would stop the CLLocationManager, decrease the Accuracy from kCLLocationAccuracyBest to kCLLocationAccuracyNearestTenMeters AND increase the distance filter from None to 50 meters and it would then track for 24+ hours.
CLLocationManager.requestLocation() takes around 10 seconds to fire didUpdateLocations event.
Here are the attributes set for the CLLocationManager
let locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = 10
locationManager.requestWhenInUseAuthorization()
locationManager.requestLocation()
As per the documentation this can take several seconds.
This method returns immediately. Calling it causes the location manager to obtain a location fix (which may take several seconds) and call the delegate’s locationManager(_:didUpdateLocations:) method with the result.
But can this take 10 long seconds? Or am I missing something?
If you switch out the
locationManager.requestLocation()
for
locationManager.startUpdatingLocation()
then didUpdateLocations will start firing immediately. This should solve your problem.
TL;DR
requestLocation() is a convenient method provided by Apple which under the hood will run startUpdatingLocation() , retrieve multiple location data, and select the most accurate one to pass to delegate, and call stopUpdatingLocation()
This process can take up to 10 seconds (which is around the timeout limit) if it can't decide which location data is the best.
I think you have a false premise. That Google is always faster. I'm guessing that your building app from scratch and the app has no access to cache. Otherwise GoogleMaps can also sometimes take more than 3 seconds. Obviously I don't know the exact specifics but I just think when you're using GoogleMaps you're using it as a user and now when you're developing your own app you're thinking about it as a developer ie you're being more meticulous about it.
Also to have the best of comparisons make sure you set your desiredAccuracy to BestForNavigation, distanceFilter to 0 and activityType to .automotive. That's normally what navigation apps are doing.
Leo's comment is also important: Make sure you update the UI from the main queue
And as mentioned by both highly experienced in Core-Location users: programmer and Paulw11:
When you call startUpdatingLocation on the location manager you must give it time to get a position. You should not immediately call stopUpdatingLocation. We let it run for a maximum of 10 seconds or until we get a non-cached high accuracy location.
I changed .desiredAccuracy to kCLLocationAccuracyKilometer and my .requestLocation() now returns the position immediately
I found the following line in my Console logs:
Ignoring requestLocation due to ongoing location.
No idea what's going on. My app doesn't call startUpdatingLocation() at all. But when I keep the app running I can see that locationManager(_:didUpdateLocations:) is called periodically.
Once I put stopUpdatingLocation() in front of the requestLocation() it's as fast as expected:
locationManager.stopUpdatingLocation()
locationManager.requestLocation()
I am working on some internal location tracking, just to see how accurate it can get (not very). The app is never going on the app store
Can I just check I have done everything to enable background iBeacons updates (didRangeBeacons) to happen, it does seem to work but die 20 mins later. This could be useful to others as there seemed mixed info out there.
I am using iOS 9
Enable location updates in Capabilities
On each region enable region.notifyEntryStateOnDisplay = true
On Location Manager enable locationManager.allowsBackgroundLocationUpdates
locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers (set this to save battery
Implement didUpdateLocations delegate method but you don't need to do anything here
I also implemented didUpdateToLocation delegate method just for belts and braces
Make sure you have the pList entries for Location Manager (NSLocationAlwaysUsageDescription, NSLocationWhenInUseDescription)