iOS App Background Location (Push Notifications) - objective-c

I face an issue where my app requires the users location when they receive a push notification while the app is running in the background.
After doing a bit of reading I'm under the impression that this isn't possible, i.e:
1) App in background
2) Push notification received
3) Get users location and execute block of code.
I found this post on here that suggests you can run a task every N amount of minutes to get the users location. So using this I could check say every ten minutes which would work but it would be a waste of the users battery if I kept activating the GPS. Therefore I was wondering can I tell within my background task if the user has any push notification waiting for my app? That way I would check if they did every ten minutes and only activate the GPS if they did.
Perhaps I'm going about this all the wrong way, I would really appreciate any advice. Sorry for not having any of the code for the background task yet as I'm still trying to get it to work.
[UPDATE]
Thanks Dustin for your advice. After a bit of testing I've decided to go with an easier option. I am now using the Apple Significant Location Changes. I store the changes in an array, I've set this to only update if the last change was greater than 10min ago. Then when the app becomes active I match the closest time in the array to the time the push notification was sent and use that location.

As discussed I used alternative solution, here it is:
Under viewDidLoad or didFinishingLaunchingWithOptions add:
locationsLogged = [[NSMutableArray alloc] init];
lastLocationTime = [[NSDate alloc] init]
locationManager = [[CLLocationManager alloc] init];
[locationManager setDelegate:self];
[locationManager setDesiredAccuracy:kCLLocationAccuracyBest];
Under applicationDidEnterBackground add:
[locationManager startMonitoringSignificantLocationChanges];
Add:
-(void) storeLocations:(CLLocation *)location
{
bgTask = [[UIApplication sharedApplication]
beginBackgroundTaskWithExpirationHandler:
^{
[[UIApplication sharedApplication] endBackgroundTask:bgTask];
}];
[locationsLogged addObject:location];
if (bgTask != UIBackgroundTaskInvalid)
{
[[UIApplication sharedApplication] endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}
}
-(void) locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation
{
BOOL isInBackground = NO;
if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground)
{
isInBackground = YES;
}
NSDate *now = [NSDate date];
NSTimeInterval diff = [now timeIntervalSinceDate:lastLocationTime];
if ((int)diff > 600) {
if (isInBackground)
{
[lastLocationTime release];
lastLocationTime = [[NSDate alloc] init];
[self storeLocations:newLocation];
}
else
{
// ...
}
}
}
Then on applicationDidBecomeActive you can use whatever logic you need to apply location information.

Related

CLLocationManager updateLocation never called while app is in background

I've read so many question here at stackoverflow and I am still having issues with CLLocationManager.I have already added keys in info.plist (NSLocationAlwaysAndWhenInUseUsageDescription,NSLocationWhenInUseUsageDescription,NSLocationAlwaysAndWhenInUseUsageDescription). My app supports ios 9.0 to 11.x.
Update:- I'm testing on iphone6 ios 11.0.3 physical device
My Approach -
1. Start updating location after while using the app permission.
2. When app goes into background stop location manager to remove Blue Banner (Banner Of Shame)
3.Fire a periodic timer of 30 seconds and start location manager again.
This time I never got the delegate callback didUpdateLocation
I have a singleton class called LocationManager.
Here is my code from LocationManager and AppDelegate
LocationManager
- (void)startLocatingUser {
//Locate User
_locationMeasurements = [NSMutableArray array];
self.geocoder = [[CLGeocoder alloc] init];
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
self.locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters;
self.locationManager.distanceFilter = kCLDistanceFilterNone;
self.locationManager.pausesLocationUpdatesAutomatically = NO;
self.locationManager.activityType = CLActivityTypeAutomotiveNavigation;
if ([self.locationManager respondsToSelector:#selector(setAllowsBackgroundLocationUpdates:)]) {
[self.locationManager setAllowsBackgroundLocationUpdates:YES];
}
if(IS_OS_8_OR_LATER) {
if ([self.locationManager respondsToSelector:#selector(requestAlwaysAuthorization)]) {
[self.locationManager requestAlwaysAuthorization];
}
}
if (#available(iOS 11.0, *)) {
self.locationManager.showsBackgroundLocationIndicator = NO;
}
[self.locationManager startUpdatingLocation];
}
- (void)stopLocatingUser {
if(self.locationManager) {
[self.locationManager stopUpdatingLocation];
}
}
AppDelegateCode
- (void)applicationWillResignActive:(UIApplication *)application {
_isBackgroundMode = YES;
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
LocationManager* locationManager = [LocationManager sharedLocationManager];
[locationManager stopLocatingUser];
__block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
self.bgTimer = [NSTimer scheduledTimerWithTimeInterval:30.0
target:self
selector:#selector(startTrackingBg)
userInfo:nil
repeats:YES];
}
-(void)startTrackingBg {
dispatch_async(dispatch_get_main_queue(), ^{
LocationManager* locationManager = [LocationManager sharedLocationManager];
[locationManager startLocatingUser];
});
NSLog(#"App is running in background");
}
I am never getting this delegate callback in background once I stop and start location manager again.
- (void)locationManager:(CLLocationManager *)manager
didUpdateLocations:(NSArray *)locations
What I simply want is whenever user puts the app in background. I want to hide the banner of shame and then I need periodic location updates in background and send them to server.

Display background notification when CLProximityNear

I am trying to display a notification when my app is in the background and the device enters a region of an iBeacon and when their CLProximity is Near the notification is working, but it keeps appearing at 1 second intervals:
- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region{
NSLog(#"Entered beacon region");
[self.locationManager startRangingBeaconsInRegion:self.beaconRegion];
}
- (void) locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region{
NSLog(#"Left region");
[self.locationManager stopRangingBeaconsInRegion:self.beaconRegion];
}
- (void) locationManager:(CLLocationManager *)manager didStartMonitoringForRegion:(CLRegion *)region {
[self.locationManager startRangingBeaconsInRegion:self.beaconRegion];
}
- (void) locationManager:(CLLocationManager *)manager didRangeBeacons:(NSArray *)beacons inRegion:(CLBeaconRegion *)region {
CLBeacon *beacon = [[CLBeacon alloc] init];
beacon = [beacons lastObject];
self.uuidLabel.text = beacon.proximityUUID.UUIDString;
if(beacon.proximity == CLProximityUnknown) {
distanceLabel.text = #"Unknown Proximity";
[self.view setBackgroundColor:[UIColor grayColor]];
} else if (beacon.proximity == CLProximityImmediate) {
distanceLabel.text = #"Immediate";
[self.view setBackgroundColor:[UIColor redColor]];
} else if (beacon.proximity == CLProximityNear) {
distanceLabel.text = #"Near";
[self.view setBackgroundColor:[UIColor orangeColor]];
UILocalNotification *inRange = [[UILocalNotification alloc] init];
inRange.alertBody = [NSString stringWithFormat:#"Entered region!"];
inRange.soundName = UILocalNotificationDefaultSoundName;
[[UIApplication sharedApplication] presentLocalNotificationNow:inRange];
} else if (beacon.proximity == CLProximityFar) {
distanceLabel.text = #"Far";
[self.view setBackgroundColor:[UIColor blueColor]];
}
}
Should there be a method call after the notification is displayed to tell the app that it has been displayed and not to keep calling the didRangeBeacons method until the user goes out of range and back in again?
When ranging beacons, locationManager:didRangeBeacons:inRegion will be called every second, as you've identified. Each time, the beacons parameter will contain an array of all visible beacons.
It's up to your app to contain logic to determine whether a new beacon is visible, or whether you've already notified the user about it. I suggest you store an array of previously discovered beacons, and each time locationManager:didRangeBeacons:inRegion is called you compare your list with the contents of the beacons parameter. You then should be able to tell whether any new beacons have been found.
The multiple notifications can be solved like this:
If you only want the notification to be sent once, simply define an alreadyDisplayed flag, which gets set after the notification is sent, then check its value before sending.
Like this:
BOOL alreadyDisplayed = NO;
...
- (void) locationManager:(CLLocationManager *)manager didRangeBeacons:(NSArray *)beacons inRegion:(CLBeaconRegion *)region {
...
else if (beacon.proximity == CLProximityNear) {
distanceLabel.text = #"Near";
if (!alreadyDisplayed) {
[self.view setBackgroundColor:[UIColor orangeColor]];
alreadyDisplayed = YES;
UILocalNotification *inRange = [[UILocalNotification alloc] init];
inRange.alertBody = [NSString stringWithFormat:#"Entered region!"];
inRange.soundName = UILocalNotificationDefaultSoundName;
[[UIApplication sharedApplication] presentLocalNotificationNow:inRange];
}
}
...
}
But you still have a second problem:
If you want to do this in the background as your question title suggests, this isn't going to work at all. The problem is that after iOS detects that your phone entered an iBeacon region in the background, it only lets it run for five seconds before putting it to sleep. Because the range of an iBeacon is about 50 meters, the most likely case is that this five second interval will start when you are at the edge of that 50 meter range. It is very unlikely that the user will walk so fast that they enter the "Near" proximity within the 5 seconds you have before your app is put to sleep. For this reason, it is generally not possible to take specific action based upon an specific proximity when you are in the background.
That said, if you want to do this in the foreground, this will work just fine if you make changes to keep the notifications from coming every second.

Can we show user location in offline in MKMapview

Can we get current user location "That blue ball which is animating" when our device is offline.when i tried to get current user location and log it i'm getting 0.00000 for both longitude and latitude here is the code that i used to get current user location.I'm using ipad mini to test it.I have also added CLLocationManagerDelegate in .h file.
- (void)viewDidLoad {
[self getUserLocation];
self.myMapView.showsUserLocation = YES;
}
-(void)getUserLocation{
if ([CLLocationManager locationServicesEnabled]) {
locationManager = [[[CLLocationManager alloc] init] autorelease];
locationManager.delegate = self;
locationManager.distanceFilter = kCLDistanceFilterNone;
locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation;
[locationManager startUpdatingLocation];
}else{
NSLog(#"User location Disabled");
}
}
- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation
{
CLLocation *currentGPSLocation = newLocation;
if (currentGPSLocation != nil) {
currentLocation.latitude = self.myMapView.userLocation.coordinate.latitude;
currentLocation.longitude = self.myMapView.userLocation.coordinate.longitude;
currentLocationWalk.latitude=self.myMapView.userLocation.coordinate.latitude;
currentLocationWalk.longitude=self.myMapView.userLocation.coordinate.longitude;
NSLog(#"didUpdateToLocation: %f,%f", self.myMapView.userLocation.coordinate.latitude,self.myMapView.userLocation.coordinate.longitude);
statusLabel.textColor = [UIColor blackColor];
statusLabel.text = [NSString stringWithFormat:#"%f",self.myMapView.userLocation.coordinate.latitude];
statusLabel1.text = [NSString stringWithFormat:#"%f",self.myMapView.userLocation.coordinate.longitude];
}
}
You are not using ARC (I strongly suggest using it, it avoid most of the memory management errors!).
Therefore it might be that "locationManager" is a instance variable that is not retained (maybe you did not declare it as a retained property).
In this case, it might have been released already when your code returns to the main run loop, where the autorelease pool is drained.
This line
locationManager = [[[CLLocationManager alloc] init] autorelease];
is not a good idea.
define the locationManager as a property,
init the locationManager in viewDidLoad(), but remove the autorelease!
and relase the locationManager only when the viewController is unloaded (there where you release the other properties)

didUpdateToLocation called multiple

i have an iOS application witch uses the current location of the user. I am doing like this :
-(void)startGeoloc{
NSLog(#"start geoloc");
locationManager = [[CLLocationManager alloc] init];
locationManager.delegate = self;
locationManager.desiredAccuracy=kCLLocationAccuracyHundredMeters;
[locationManager startUpdatingLocation];
}
#pragma mark - CLLocationManagerDelegate methods
- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation
{
[locationManager stopUpdatingLocation];
AppDelegate *apDelegate =(AppDelegate *)[UIApplication sharedApplication].delegate;
apDelegate.modeGeoloc = YES;
[self callWebService:locationManager.location];
}
The problem of this, is that my method callWebService:locationManager.location is called twice and i would like to call it just one time. how i can i do this ? thanks for your answers
To ensure locationManager only calls the "didUpdate..." method once, use a BOOL to reference if the location has been found yet.
Create the ivar BOOL:
#property BOOL didFindLocation;
Before locationManager startUpdatingLocation, set the new BOOL to NO. That way you can call for a new location update at will.
-(void) startFindingLocation {
self.didFindLocation = NO; // like this
locationManager = [[CLLocationManager alloc] init];
[locationManager setDelegate:self]
[locationManager setDesiredAccuracy:kCLLocationAccuracyHundredMeters];
[locationManager startUpdatingLocation];
}
In "didUpdateLocations", check for it.
- (void) locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations {
if (!self.didFindLocation) {
self.didFindLocation = YES;
[locationManager stopUpdatingLocation];
// do the rest of your stuff
}
}
If possible, do not set didFindLocation anywhere else in your code to avoid confusion.
locationManager delegate methods can be called very frequently (they didUpdateToLocation all the time, right? :)
One way would be to have your callWebService have state, know whether it is currently executing a request and ignore concurrent requests if one is still going. Another way would be to keep a timestamp and only allow it through if 2 minutes has passed since the previous one.
Had the same problem.
I think the easiest solution is setting the CLLocationManager to null.
locationManager = nil;
after calling
[locationManager stopUpdatingLocation];
locationManager.startUpdatingLocation() fetch location continuously and didUpdateLocations method calls several times,
Just set the value for locationManager.distanceFilter value before calling locationManager.startUpdatingLocation().
As I set 100 meters(you can change as your requirement) working fine, and will work for you.
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = 100
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()

iOS Background task (NSThread) dies

Some days ago I posted a question about NSThread. Finally I managed to make it run in background every minute.
The aim of the app is to get the location and then call to a webservice to update it on a server with ASIHTTPRequest every X minutes.
The code:
- (void)threadEntryPoint:(ActualizarPosicionGPS2AppDelegate *)paramSender{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
while([[NSThread currentThread] isCancelled] == NO){
[NSThread sleepForTimeInterval:60.0f];
if ([[NSThread currentThread] isCancelled] == NO &&
[[UIApplication sharedApplication] backgroundTimeRemaining] != DBL_MAX) {
[self hacerCosasBackground];
}
}
[pool release];
}
hacerCosasBackground calls to the locationManager updating it's position and then uploads the position to a server.
In application:didFinishLaunchingWithOptions:
[NSThread detachNewThreadSelector:#selector(threadEntryPoint:) toTarget:self withObject:self];
-(void) endTaskWidthIdentifier:(NSNumber *)paramIdentifier{
UIBackgroundTaskIdentifier identifier = [paramIdentifier integerValue];
[[UIApplication sharedApplication] endBackgroundTask:identifier];
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
self.backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^{
NSNumber *backgroundTask = [NSNumber numberWithInteger:self.backgroundTaskIdentifier];
[self performSelectorOnMainThread:#selector(endTaskWidthIdentifier:) withObject:backgroundTask waitUntilDone:YES];
self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
}];
}
However, the NSThread dies about 10 minutes later of closing the app. I've registered in the info.plist the 'UIBackgroundModes' location.
I also read about startMonitoringSignificantLocationChanges but I want to make my app very configurable (the user will be able to set the how often they want to update the location, etc.) and I understood that that method updates every 500m (that's not good, I want to update the location more often).
What could I do? Any help please?
Thank you very much!