I've implemented in my app the region monitoring feature of CLLocationManager, it works but it kills my battery:
-
-
Is it should be like that?
My code:
monitorLocationViewController.m (please scroll to see the full code):
-(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations
{
//If "allStores"(NSMutableArray) isn't nil - calling "locationChangeHandler" to update monitoring
if (self.allStores!=nil) {
[self locationChangeHandler];
}
CLLocation *currentLocation=(CLLocation*)[locations lastObject];
NSSet *monitoredRegionsSet=self.locationManager.monitoredRegions;
[monitoredRegionsSet enumerateObjectsUsingBlock:^(CLCircularRegion *region, BOOL *stop) {
if ([region containsCoordinate:currentLocation.coordinate]) {
[self.locationManager stopMonitoringForRegion:region];
[self locationManager:self.locationManager didEnterRegion:region];
}
}];
}
-(void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region
{
Store *store=[self storeForRegion:region];
if (store.alreadySendNotification==NO) {
UILocalNotification *notification=[[UILocalNotification alloc] init];
notification.alertTitle=#"Arounder";
notification.alertBody=[[self storeForRegion:region] address];
[[UIApplication sharedApplication] scheduleLocalNotification:notification];
store.alreadySendNotification=YES;
}
}
//For updating monitoring
-(void)locationChangeHandler
{
//If "allStores"(NSMutableArray) isn't nil
if (self.allStores!=nil) {
//Finding the 20 closest stores to he user's location and adding it to "twentyClosestStores"(NSMutableArray)
[self sortClosestStores];
//Stop monitoring "previousTwentyStores"(NSMutableArray) (20 closest stores before user's location updated)
[self stopMonitoringStores];
//Start monitoring "twentyClosestStores"(NSMutableArray)
[self startMonitoringClosestStores];
}
}
//Start monitoring "twentyClosestStores"(NSMutableArray)
-(void)startMonitoringClosestStores
{
//If monitoring isn't availible for "CLCircularRegion"
if (![CLLocationManager isMonitoringAvailableForClass:[CLCircularRegion class]]) {
NSLog(#"Monitoring is not available for CLCircularRegion class");
return;
}
//Run on all "twentyClosestStores"(NSMutableArray)'s objects
for (Store *currentStore in self.twentyClosestStores) {
//Start monitoring "region"(CLCircularRegion)
[self.locationManager startMonitoringForRegion:currentStore.circularRegion];
}
}
//Stop monitoring "previousTwentyStores"(NSMutableArray) (20 closest stores before user's location updated)
-(void)stopMonitoringStores
{
//Run on all "monitoredRegions"(NSSet) of "locationManager"(CLLocationManager) objects
for (CLCircularRegion *currentRegion in self.locationManager.monitoredRegions) {
//Stop monitoring "region"(CLCircularRegion)
[self.locationManager stopMonitoringForRegion:currentRegion];
}
}
//Finding a store for region
-(Store*)storeForRegion:(CLCircularRegion*)region
{
//Run on all "allStores"(NSMutableArray)'s objects
for (Store *currentStore in self.allStores) {
//If "currentStore"(Store)'s "circularRegion"'s identifier is equal to "region"(CLCircularRegion)'s identifier
if ([currentStore.circularRegion.identifier isEqualToString:region.identifier]) {
//Returning "currentStore"(Store)
return currentStore;
}
}
//Store not found - returning nil
NSLog(#"No store found for this region: %f,%f",region.center.latitude,region.center.longitude);
return nil;
}
AppDelegate.m:
-(BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.monitorLocationVC=[[monitorLocationViewController alloc] init];
self.monitorLocationVC.locationManager=self.locationManager;
[self configureLocationManager];
[self.locationManager startUpdatingLocation];
return YES;
}
-(void)configureLocationManager
{
//Initializing locationManager
self.locationManager=[[CLLocationManager alloc] init];
//setting "locationManager"'s(CLLocationManager) delegate to "self"
self.locationManager.delegate=self.monitorLocationVC;
//Setting "locationManager"'s(CLLocationManager)'s distance filter to none
self.locationManager.distanceFilter=kCLDistanceFilterNone;
//Setting "locationManager"'s(CLLocationManager)'s activityType to navigation
self.locationManager.activityType=CLActivityTypeAutomotiveNavigation;
//setting "locationManager"'s(CLLocationManager) desiredAccuracy to "best"
self.locationManager.desiredAccuracy=kCLLocationAccuracyBestForNavigation;
self.locationManager.pausesLocationUpdatesAutomatically=NO;
//If OS version is 9 or above - setting "allowsBackgroundLocationUpdates" to YES
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 9) {
self.locationManager.allowsBackgroundLocationUpdates = YES;
}
}
Thank you!
You only want to monitor regions, not update their location constantly in the background.
Try this:
self.locationManager.desiredAccuracy=kCLLocationAccuracyBest;
Do you really need the distanceFilter set to kCLDistanceFilterNone? That will cause more battery power to be used. You probably want to try to set that to around 10, 20, 50 or even 100 meters.
Also, in order to not update locations constantly, instead of:
[self.locationManager startUpdatingLocation];
Try just using:
[self.locationManager startMonitoringSignificantLocationChanges];
All of these things should contribute to less battery usage. When you set accuracy and distance filters to the highest possible setting, the battery is going to be drained.
EDIT:
You are going to eat up a lot of battery whatever you do because of the purpose of your app. A solution I've done before with a problem similar to this is to create algorithm or formula with an NSTimer that fires every x minutes to update the user's location. (but only update regions if they have moved x meters).
stop location updates between firing of the NSTimer so that you aren't constantly updating locations.
when the timer fires, resume location updates, grab about 10 locations (so you get an accurate one), then shut off location updates until the next time the timer is fired
Related
I have a location app that needs to get accurate location periodically. Currently I am getting constantly getting location in didUpdateLocation but I only ever log the location every 5 seconds. I am interested in a solution that gets accurate location periodically or on signification change. I would like either or both of these scenarios:
(by very accurate, I need 10m of desired accuracy)
Get a very accurate location every 5 seconds
Notify/callback if user moves a threshold ( eg moves 5 - 10 meters)
The app needs to work when backgrounded as well and location must still be logged if user switches to another app.
I was considering turning on/off location every 5 seconds but was not sure if that is the best practice. I also know there is also allowDeferredLocationUpdatesUntilTraveled but I believe that only applied to backgrounded mode. I would appreciate a solution that saves battery when the app is in use and in background mode. Please share your solutions and best practices for my use case.
I did write an app using Location services, app must send location every 10s. And it worked very well.
Just use the "allowDeferredLocationUpdatesUntilTraveled:timeout" method, following Apple's doc.
Steps are as follows:
Required: Register background mode for update Location.
Create LocationManger and startUpdatingLocation, with accuracy and filteredDistance as whatever you want:
-(void) initLocationManager
{
// Create the manager object
self.locationManager = [[[CLLocationManager alloc] init] autorelease];
_locationManager.delegate = self;
// This is the most important property to set for the manager. It ultimately determines how the manager will
// attempt to acquire location and thus, the amount of power that will be consumed.
_locationManager.desiredAccuracy = 45;
_locationManager.distanceFilter = 100;
// Once configured, the location manager must be "started".
[_locationManager startUpdatingLocation];
}
To keep app run forever using "allowDeferredLocationUpdatesUntilTraveled:timeout" method in background, you must restart updatingLocation with new parameter when app moves to background, like this:
- (void)applicationWillResignActive:(UIApplication *)application {
_isBackgroundMode = YES;
[_locationManager stopUpdatingLocation];
[_locationManager setDesiredAccuracy:kCLLocationAccuracyBest];
[_locationManager setDistanceFilter:kCLDistanceFilterNone];
_locationManager.pausesLocationUpdatesAutomatically = NO;
_locationManager.activityType = CLActivityTypeAutomotiveNavigation;
[_locationManager startUpdatingLocation];
}
App gets updatedLocations as normal with "locationManager:didUpdateLocations:" callback:
-(void) locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
// store data
CLLocation *newLocation = [locations lastObject];
self.userLocation = newLocation;
//tell the centralManager that you want to deferred this updatedLocation
if (_isBackgroundMode && !_deferringUpdates)
{
_deferringUpdates = YES;
[self.locationManager allowDeferredLocationUpdatesUntilTraveled:CLLocationDistanceMax timeout:10];
}
}
But you should handle the data in then "locationManager:didFinishDeferredUpdatesWithError:" callback for your purpose
- (void) locationManager:(CLLocationManager *)manager didFinishDeferredUpdatesWithError:(NSError *)error {
_deferringUpdates = NO;
//do something
}
NOTE: I think we should reset parameters of LocationManager each time app switches between background/forgeround mode.
Hopefully this should help
I have an app that will notify the user every time he approaches to one of my client's stores. There are more than 20 stores, so I have a function that takes the user's location and finds the 20 nearest stores to him and start monitoring the location of these stores, every time the user moves, the app finds the 20 nearest stores again, removes the previous stores from monitoring and start monitoring the new ones.
For some reason, it doesn't work, I'll be happy if one of you (or more :)) will help me to find the problem, Thanks!!
myCode (scroll to see the full code):
Note: the CLLocationManager created on the AppDelegate.m and it's delegate is this class (UIViewController).
-(void)sortClosestStores
{
[self.allStores sortUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) {
CLLocation *location1=[[CLLocation alloc] initWithLatitude:((Store*)obj1).geoPoint.latitude longitude:((Store*)obj1).geoPoint.longitude];
CLLocation *location2=[[CLLocation alloc] initWithLatitude:((Store*)obj2).geoPoint.latitude longitude:((Store*)obj2).geoPoint.longitude];
float dist1 =[location1 distanceFromLocation:self.locationManager.location];
float dist2 = [location2 distanceFromLocation:self.locationManager.location];
if (dist1 == dist2) {
return NSOrderedSame;
}
else if (dist1 < dist2) {
return NSOrderedAscending;
}
else {
return NSOrderedDescending;
}
}];
if (self.twentyClosestStores==nil) {
self.twentyClosestStores=[NSMutableArray array];
}
if (self.previousTwentyStores==nil) {
self.previousTwentyStores=[NSMutableArray array];
}
self.previousTwentyStores=self.twentyClosestStores;
self.twentyClosestStores=[NSMutableArray array];
for (int i = 0; i < 20; i++) {
[self.twentyClosestStores addObject:[self.allStores objectAtIndex:i]];
}
}
-(void)startMonitoringClosestStores
{
if (![CLLocationManager isMonitoringAvailableForClass:[CLCircularRegion class]]) {
NSLog(#"Monitoring is not available for CLCircularRegion class");
}
for (Store *currentStore in self.twentyClosestStores) {
CLCircularRegion *region=[currentStore createCircularRegion];
[self.locationManager startMonitoringForRegion:region];
}
}
-(void)stopMonitoringStores
{
for (Store *currentStore in self.previousTwentyStores) {
CLCircularRegion *region=[currentStore createCircularRegion];
[self.locationManager stopMonitoringForRegion:region];
}
}
-(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations
{
if (self.allStores!=nil) {
[self sortClosestStores];
[self stopMonitoringStores];
[self startMonitoringClosestStores];
}
}
-(void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region
{
NSLog(#"Entered"); //Not called even when the user enters one of the regions.
}
Can you please help me? Thanks!
I'm pretty new at CoreLocation myself but I would think that it is not a good idea to call stopMonitoringForRegions and startMonitoringForRegions in didUpdateLocations.
Since you're monitoring regions, the didEnterRegion delegate is what you will be interested in. That will give you the 'hey, I arrived at the X store' event, and in there is where you would probably want to call the code that you currently have in your didUpdateLocations.
You will want to setup CoreLocation probably in your AppDelegate, so you might have something like (sorry about it being Swift, that's what I'm working in right now):
locationManager.delegate = self
// auths:
locationManager.requestAlwaysAuthorization()
locationManager.requestWhenInUseAuthorization()
// config:
locationManager.distanceFilter = kCLDistanceFilterNone
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.pausesLocationUpdatesAutomatically = true
locationManager.allowsBackgroundLocationUpdates = true
locationManager.activityType = CLActivityType.AutomotiveNavigation
// start:
locationManager.startMonitoringSignificantLocationChanges()
Then you would have your code:
if (self.allStores!=nil) {
[self sortClosestStores];
[self stopMonitoringStores];
[self startMonitoringClosestStores];
}
Note: I don't think it matters if you call startMonitoringSignificantLocationChanges before or after adding monitored regions, I haven't gotten quite that far in my code yet.
didUpdateLocations is more for when you want to track location e.g. tracking a bicycle ride or jogging session.
Additional explanation:
OK, I think I understand the issue now. There are two aspects to what you want to accomplish:
being notified when the user enters a store's region
dynamically recalculating the nearest N stores as the device moves
My previous answer was geared towards the first issue.
Regarding the second issue, dynamically recalulating nearest N, the code in your didUpdateLocations will not be called unless you tell the location manager to startUpdatingLocation. Just off the top of my head:
// configure the location manager in AppDelegate:
// You will need to experiment with these two properties,
// you probably don't want to use kCLLocationAccuracyBestForNavigation.
// Maybe kCLLocationAccuracyKilometer would be sufficient.
locmgr.distanceFilter = n
locmgr.desiredAccuracy = m
locmgr.pausesLocationUpdatesAutomatically = true
locmgr.startUpdatingLocation()
// this is the delegate that will receive the events due to locmgr.startUpdatingLocation
locationManager:didUpdateLocation {
// Unless you have a specific need for it, I would refactor so that
// you don't need self.PreviousTwentyStores:
[self stopMonitoringStores];
[self sortClosestStores];
[self startMonitoringClosestStores];
}
locationManager:didEnterRegion {
// you are in the region surrounding one of the stores.
}
Alternately, consider just setting a timer and waking the app every N seconds or minutes to recalculate the nearest stores.
As I understand the various aspects of CoreLocation
startUpdatingLocation -> didUpdateLocations -> stopUpdatingLocation
is (in one sense) entirely separate from:
startMonitoringForRegion -> didEnterRegion | didExitRegion -> stopMonitoringForRegion
Additionally, startUpdatingLocation was never called so your didUpdateLocation was never called.
I'm trying to implement the deferred location updates to have a better battery consumption.
I'm starting my location manager like this :
- (void)initCoreLocation
{
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
self.locationManager.distanceFilter = kCLDistanceFilterNone;
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
self.locationManager.pausesLocationUpdatesAutomatically = YES;
self.locationManager.activityType = CLActivityTypeAutomotiveNavigation;
//Très important pour iOS9 !
if ([self.locationManager respondsToSelector:#selector(allowsBackgroundLocationUpdates)]) {
self.locationManager.allowsBackgroundLocationUpdates=YES;
}
if ([self.locationManager respondsToSelector:#selector(requestAlwaysAuthorization)]) {
[self.locationManager requestAlwaysAuthorization];
}
[self.locationManager startUpdatingLocation];
[self.locationManager startMonitoringSignificantLocationChanges];
}
And starting deferred update like this way:
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
if (!self.deferringUpdates) {
[self.locationManager allowDeferredLocationUpdatesUntilTraveled:CLLocationDistanceMax timeout:30];
self.deferringUpdates = YES;
}
}
-(void)locationManager:(CLLocationManager *)manager didFinishDeferredUpdatesWithError:(NSError *)error { // Stop deferring updates
if(error) {
NSLog(#"error");
}
NSLog(#"didFinishDeferredUpdates");
self.deferringUpdates = NO;
}
I have didFinishDeferredUpdates log every 30 seconds, however didUpdateLocations keeps calling every second, removing any try to optimise the battery consumption. Is it supposing to the location manager to call the didUpdateLocations every 30 seconds ?
Maybe you are not having the right approach as allowDeferredLocationUpdatesUntilTraveled() tells the GPS hardware to store new locations internally until the specified distance or timeout conditions are met.
From the iOS Developer Library:
If your app is in the foreground, the location manager does not defer the deliver of events but does monitor for the specified criteria. If your app moves to the background before the criteria are met, the location manager may begin deferring the delivery of events. Link
Are you debugging?
As stated in the answer here: ios deferred location updates fail to defer
Deferred updates are delivered only when the system enters a low power state. Deferred updates do not occur during debugging because Xcode prevents your app from sleeping and thus prevents the system from entering that low power state.
I believe you only need one of them at one time, change the call in applicationDidEnterBackground and applicationWillEnterForeground
- (**void)applicationDidEnterBackground:(UIApplication *)application {
// Need to stop regular updates first
[self.locationManager stopUpdatingLocation];
// Only monitor significant changes
[self.locationManager startMonitoringSignificantLocationChanges];
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
[self.locationManager stopMonitoringSignificantLocationChanges];
[self.locationManager startUpdatingLocation];
}
I am testing on an iPad 3rd gen on iOS 7.1 since I have no other iOS device for the moment.
The first time I run my app, it starts monitoring for several regions. The status bar and the Location Services settings page are showing the outlined location services icon (my app is the only one in the list that has the outlined icon). When I kill my app, the icon is still showing on both places since I do not stop monitoring the regions yet. Until then everything is fine.
My problem is when I run my app for a second time, I stop monitoring for all monitored regions, but the location services outlined icon does not disappear on the status bar and the Location Services settings page...
Here is my code called at the first run :
- (void) getLocationManagerInstance {
if (!self.locationManager) {
self.locationManager = [CLLocationManager new];
}
self.locationManager.delegate = self;
}
- (void) startLocationGathering {
if(self.shouldUpdateGPSLocations) {
[self.locationManager startMonitoringSignificantLocationChanges];
}
}
- (void) startMonitoringBeaconRegions {
if(self.rootRegion) {
[self.locationManager startMonitoringForRegion:self.rootRegion];
}
if (self.beaconRegions && self.beaconRegions.count < 20) {
[self.beaconRegions enumerateObjectsUsingBlock:^(CLBeaconRegion* region, NSUInteger idx, BOOL *stop) {
[self.locationManager startMonitoringForRegion:region];
}];
}
}
- (void) startMonitoringCircularRegions {
if (self.gpsRegions && self.gpsRegions.count) {
[self.gpsRegions enumerateObjectsUsingBlock:^(CLCircularRegion* region, NSUInteger idx, BOOL *stop) {
[self.locationManager startMonitoringForRegion:region];
}];
}
}
And my code called at the second run :
- (void) getLocationManagerInstance {
if (!self.locationManager) {
self.locationManager = [CLLocationManager new];
}
self.locationManager.delegate = self;
}
- (void) locationManagerCleanup {
[AWRUtils dlog:#"locationManagerCleanup"];
NSArray* monitoredRegions = [self.locationManager monitoredRegions].allObjects;
for (CLRegion* r in monitoredRegions) {
[self.locationManager stopMonitoringForRegion:r];
}
NSArray* rangedRegions = [self.locationManager rangedRegions].allObjects;
for (CLBeaconRegion* r in rangedRegions) {
[self.locationManager stopRangingBeaconsInRegion:r];
}
[self.locationManager stopUpdatingLocation];
[self.locationManager stopMonitoringSignificantLocationChanges];
}
If I uninstall my app, the outlined location services icon disappear. But why is the icon not disappearing when I stop monitoring the monitored regions?
EDIT: After more testing, I found that the instance of the CLLocationManager that I have on the second run has no monitored regions ([self.locationManager monitoredRegions] returns nil)...
EDIT 2: I also found that if, on the second run, I start monitoring all the same regions that started monitoring on the first run AND THEN I stop monitoring them, the outlined location services icon disappear. Is this a normal behavior? I read nothing about that in all my internet researches...
Hi I am implementing Location services in my app. First I have to know my Coordinates to get the distance between some places that I have in a list and the device. Then if I go into a place I can make a check in, so, I need to get coordinates again, and the problem is here. Second time I try to get coordinates, the method -(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations is not called.. and I can not get new Coordinates.
My manager is located in a NSObject sublcass with this code:
(id)init {
if ( self = [super init] ) {
if ([CLLocationManager locationServicesEnabled])
{
locationManager = [[CLLocationManager alloc] init];
locationManager.delegate = self;
[locationManager startUpdatingLocation];
}
}
return self;
}
-(void) checkLongLatitudeAgain {
[locationManager startUpdatingLocation];
}
#pragma mark Delegates de CLLocationManager
//
-(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations{
NSLog(#"LON%f", manager.location.coordinate.longitude);
NSLog(#"LAT:%f", manager.location.coordinate.latitude);
NSTimeInterval howRecentNewLocation = [newLocationeventDate timeIntervalSinceNow];
if (manager.location.horizontalAccuracy <= 100.0 && howRecentNewLocation < -0.0 && howRecentNewLocation > -20.0){
//Usar coordenada
[self.delegate getLocationForCheckIn:manager.location];
[self stopUpdatingLocation:#"Fins"];
}
}
// ---------------------------------------------------------------------------------------------------------------------
-(void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error{
//
if ([error code] != kCLErrorLocationUnknown) {
[self stopUpdatingLocation:NSLocalizedString(#"Error", #"Error")];
}
//
}
// ---------------------------------------------------------------------------------------------------------------------
- (void)stopUpdatingLocation:(NSString *)state {
//Detenemos la lectura del GPS
[locationManager stopUpdatingLocation];
locationManager.delegate = nil;
NSLog(#"Stop gps");
//
}
I call the class when the list of places is open, and also when inside a place the user press checkIn button. Both times I do it with this code:
WPLocationManager *location = [[WPLocationManager alloc]init];
[location checkLongLatitudeAgain];
You are creating a new manager every time:
WPLocationManager *location = [[WPLocationManager alloc]init];
[location checkLongLatitudeAgain];
That new manager is not assigned to any delegate.
You need to use the previous manager you have created and assigned to your delegate, something like:
[locationManager checkLongLatitudeAgain];
You can check the documentation at http://developer.apple.com - https://developer.apple.com/library/ios/documentation/userexperience/conceptual/LocationAwarenessPG/CoreLocation/CoreLocation.html
In particular you can check the Starting the Standard Location Service and Starting the Significant-Change Location Service sections. You have to use the startMonitoringSignificantLocationChanges or startUpdatingLocation method of CLLocationManager, cache your location somewhere and update it only when a new location is received, otherwise like it is stated in the documentation: "If a location update has already been delivered, you can also get the most recent location data directly from the CLLocationManager object without waiting for a new event to be delivered".
i dont know why you are initiating your location manager again again, also even if you some how manage to solve current problem but it's not proper way of dealing with location manage based applications.I had been in trouble previously when i was working on location based app. the best approach for location based app is singleton.
apple forum discussion
you can find
this
and this very helpful.
just an advice, :)
Thanks.
In iOS8 for me I had to call [locationManager stopUpdatingLocation]; before calling [locationManager startUpdatingLocation] to start getting updates second time and it works for me.