CLLocationManager, didEnterRegion and UILocalNotification - ios

So after much trial and error and reading Apples documents and SO threads I thought I had my didEnterRegion working properly.
This is what I finished up with...
- (void)locationManager:(LocationManager *)locationManager didEnterRegion:(CLRegion *)region{
NSLog(#"Location manager did enter region called");
[self.locationManager stopUpdatingLocation];
if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateActive) {
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
UILocalNotification *localNotification = [[UILocalNotification alloc]init];
localNotification.alertBody = #"You are about to arrive";
localNotification.alertAction = #"Open";
localNotification.applicationIconBadgeNumber = [[UIApplication sharedApplication]applicationIconBadgeNumber]+1;
[[UIApplication sharedApplication]presentLocalNotificationNow:localNotification];
} else {
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"App Running in foreground notification fired");
[self setupAlarmTriggeredView];
//vibrate device
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
});
}
}
As you can see it does a simple check to see if the app is active, if not it sets up a UILocalNotification which it presents immediately, if it is it just vibrates and changes the view.
When testing on the simulator using a GPX file to move the location over the boundary, both scenarios work perfectly. However when testing the app out and about, when the app is in the background the notification doesn't seem to fire until you wake the device. ie. you can cross the boundary and nothing happens, then when you unlock the device, boom, it vibrates and the view is changed accordingly. (if the app is not foremost when you unlock this doesn't happen until you re-launch the app).
Should I be doing this setup in appDidFinishLaunchingWithOptions? In my testing both that method and didRecieveLocalNotification: in the app delegate don't get called until 'after' the notification has been fired AND the user has re-launched the app by actioning the notification (at which point you can check the for the launch key in the options array), this doesn't seem to be of any use for the initial firing of the notification as part of didEnterRegion. At the moment I have no code in either of these methods.
As far as I'm aware I don't need to be doing any background location updates for didEnterRegion, this is handled automatically by iOS.

Related

Location Services not working in background called by WatchKit

My Apple Watch app requires some data and requests it from the corresponding iPhone app. To fulfill the request the iPhone app requires the users location.
After receiving and testing with a real Apple Watch I found out that my iPhone app does not receive location updates when running in background. If the iPhone app is active in foreground it works without issues. With the simulator it worked in both cases.
In both cases (active and background) the WatchKit extension calls and starts successfully the iPhone app and goes all the way until startUpdatingLocation is called in the iPhone app. But in case the app is running in background didUpdateLocations is never called.
I tried with requestAlwaysAuthorization as well as requestWhenInUseAuthorization. No difference.
I also activated then the "location updates" background mode within capabilities. But again no difference.
Has someone else faced the same problem and found a way to receive the location also in background?
Here some code. First the check if authorization is required.
// iOS 8 check to avoid crash on older iOS
if ([self.locationManager respondsToSelector:#selector(requestWhenInUseAuthorization)])
{
[self requestLocationAlwaysAuthorization];
}
else
{
[self runLocationUpdate];
}
Here the check for the proper Location Manager rights.
- (void)requestLocationAlwaysAuthorization
{
CLAuthorizationStatus currentAuthStatus = [CLLocationManager authorizationStatus];
if (currentAuthStatus == kCLAuthorizationStatusDenied)
{
//request user to change setting
}
else if (currentAuthStatus == kCLAuthorizationStatusRestricted)
{
//request user to change setting
}
else if (currentAuthStatus == kCLAuthorizationStatusNotDetermined)
{
[self.locationManager requestAlwaysAuthorization];
[self runLocationUpdate];
}
else if (currentAuthStatus == kCLAuthorizationStatusAuthorizedWhenInUse)
{
//maybe when in use is also enough?
[self runLocationUpdate];
}
else if (currentAuthStatus == kCLAuthorizationStatusAuthorizedAlways)
{
//all ok
[self runLocationUpdate];
}
}
Here the call of startUpdatingLocation. The didUpdateLocations delegate will only be called when iPhone app is active.
-(void)runLocationUpdate
{
[self.locationManager startUpdatingLocation];
}
Three things to check and be aware of:
Location Permissions like [self.locationManager requestAlwaysAuthorization]; are only acknowledged once by the OS. If you have already requested permission, doesn't matter the level, the OS will NOT display a request to the user. The OS will just pass over the request and leave the permission level as is. The only time you can be assured that the OS will display the request to the user is if the [CLLocationManager authorizationStatus] returns kCLAuthorizationStatusNotDetermined. In every other case, you must manually request permission by displaying an Alert or other form of UI display. Also note that the OS retains whether or not it already displayed the request, even if you delete your app and reinstall it. So to test, you need to reset your Simulator's Content or your iPhone's Location Privacy.
Make sure you have added the plist keys for NSLocationAlwaysUsageDescription AND NSLocationWhenInUseUsageDescription If you don't add this to your plist, the OS will ignore any Location Permission Requests.
If you want to use requestAlwaysAuthorization to get location data from the phone (not the watch app extension) while the phone app is in the background, will also require you register for Background Modes Location updates under Project>Target>Capabilities.
UPDATE
Use a background task to give your app time to respond when in the background. Something like this:
-(void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *replyInfo))reply{
UIApplication *app = [UIApplication sharedApplication];
UIBackgroundTaskIdentifier bgTask __block = [app beginBackgroundTaskWithName:#"watchAppRequest" expirationHandler:^{
[[UIApplication sharedApplication] endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
//make your calls here to your tasks, when finished, send the reply then terminate the background task
//send reply back to watch
reply(replyInfo);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[app endBackgroundTask:bgTask];
bgTask=UIBackgroundTaskInvalid;
});
}

How to implement didFinishLaunchingWithOptions method when the app is launched as a result of location change?

I'm using Region Monitoring in my app and it's work pretty well when I'm walking around the city with an app launched.
The problem I have is receiving those location changes notifications when my app is terminated. My app is crashing when it's waken up because of location change and it crashes when I'm trying to set rootViewController in application:didFinishLaunchingWithOptions: method.
Should I implement this method differently when my app is launched in the background as a result of location change?
How much time do I have in the background to perform my tasks when my app is waken up when entering/exciting region?
To answer your second question, iOS / Apple says you have about 10 seconds to do the necessary.
First question:
In your app delegate.h, add: CLLocationManagerDelegate
In your application:didFinishLaunchingWithOptions: method add:
locationManager = [[CLLocationManager alloc] init];
[locationManager setDelegate:self];
And also add:
- (void)locationManager:(CLLocationManager *)manager didDetermineState:(CLRegionState)state forRegion:(CLRegion *)region
{
UILocalNotification *notification = [[UILocalNotification alloc] init];
[notification setSoundName:UILocalNotificationDefaultSoundName];
[notification setApplicationIconBadgeNumber:0];
if(state == CLRegionStateInside) {
[notification setAlertBody:#"CLRegionStateInside"];
}
else if(state == CLRegionStateOutside) {
[notification setAlertBody:#"CLRegionStateOutside"];
}
[[UIApplication sharedApplication] presentLocalNotificationNow:notification];
}
Then, where you have your region code (Probably somewhere in some view controller), add: [yourRegion setNotifyEntryStateOnDisplay:YES];
before your original call to:
[locationManager startMonitoringForRegion:yourRegion];
That's all!

Background location tracking not working when the app is terminated

I have an app which is set to monitor entry into regions. On entry, the app will alert the user with a local notification.
This works fine when the app is on, or in the background. However if the app is terminated, the region monitoring never raises the local notification.
I have set my "background modes" key in the info.plist.
Could this be because my CLLocation code is not in the AppDelegate (instead it is in a singleton)?
Could this be because it's not possible to run code to raise the location notification from a terminated state?
Here's my code when the region is entered:
- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region {
UIApplication* app = [UIApplication sharedApplication];
UILocalNotification* notifyAlarm = [[UILocalNotification alloc] init];
notifyAlarm.fireDate = [NSDate dateWithTimeIntervalSinceNow:5];;
notifyAlarm.timeZone = [NSTimeZone defaultTimeZone];
notifyAlarm.repeatInterval =NSDayCalendarUnit;
notifyAlarm.alertBody = [Installation currentInstallation].reminderText;
[app scheduleLocalNotification:notifyAlarm];
}
There are 2 things happen:
a) If the application is suspended when an update occurs, the system wakes it up in the background to handle the update.
b) If the application starts this service and is then terminated, the system relaunches the application automatically when a new location becomes available.
What we can now do is turn on significant location updates when the user hits the home key and we can let the system wake us up when needed.
-(void) applicationDidEnterBackground:(UIApplication *) application
{
// You will also want to check if the user would like background location
// tracking and check that you are on a device that supports this feature.
// Also you will want to see if location services are enabled at all.
// All this code is stripped back to the bare bones to show the structure
// of what is needed.
[locationManager startMonitoringSignificantLocationChanges];
}
Then to perhaps switch to higher accuracy when the application is started up, use;
-(void) applicationDidBecomeActive:(UIApplication *) application
{
[locationManager stopMonitoringSignificantLocationChanges];
[locationManager startUpdatingLocation];
}
Next you’ll likely want to change your location manager delegate to handle background location updates.
-(void) locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation
{
BOOL isInBackground = NO;
if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground)
{
isInBackground = YES;
}
// Handle location updates as normal.
if (isInBackground)
{
// Do, if you have to send location to server, or what you need
}
else
{
// ...
}
}
Please Note: Location monitoring in background will take effect on battery usage.
This code is taken from http://www.mindsizzlers.com/2011/07/ios-background-location/

CLBeaconRegion notifyEntryStateOnDisplay and UILocalNotifications

I am trying to get a local notification to fire any time I turn on my phone and I am inside a specific region. This works as expected but each time I turn my device on I get a new notification. If I just leave the existing notification it can get pushed down to the bottom of the notification list. If I cancel the existing notification and create a new one I get a weird animation. Is there a way to either:
Update an existing UILocalNotification that has already been delivered to push it to the top.
Somehow get notified when the lock screen goes away and cancel the notification then?
Here is my existing code:
- (void)locationManager:(CLLocationManager *)manager didDetermineState:(CLRegionState)state forRegion:(CLRegion *)region {
if ([region isKindOfClass:[CLBeaconRegion class]]) {
CLBeaconRegion *beaconRegion = (CLBeaconRegion*)region;
UILocalNotification *existingNotification = self.beaconNotification;
switch (state) {
case CLRegionStateInside:
self.beaconNotification = [[UILocalNotification alloc] init];
self.beaconNotification.alertBody = #"You're inside the region";
[[UIApplication sharedApplication] presentLocalNotificationNow:self.beaconNotification];
if (existingNotification) {
[[UIApplication sharedApplication] cancelLocalNotification:existingNotification];
}
break;
case CLRegionStateOutside:
self.beaconNotification = [[UILocalNotification alloc] init];
self.beaconNotification.alertBody = #"You're outside the region";
[[UIApplication sharedApplication] cancelAllLocalNotifications];
break;
default:
break;
}
}
}
There are definitely ways to detect if the phone is locked/unlocked Is there a way to check if the iOS device is locked/unlocked?
For further notifications, I suggest you look at this list:http://iphonedevwiki.net/index.php/SpringBoard.app/Notifications
It contains the com.apple.springboard.deviceWillShutDown notification that is called when the phone is shutting down. I just tested it with this code and seems to work, however, the app gets killed immediately and the XCode session terminates so I'm not sure how useful this is for a real application.
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
NULL,
shutdownCallback,
CFSTR("com.apple.springboard.deviceWillShutDown"),
NULL,
CFNotificationSuspensionBehaviorDeliverImmediately);
void shutdownCallback (CFNotificationCenterRef center, void *observer,
CFStringRef name, const void *object, CFDictionaryRef userInfo)
{
NSLog(#"Shutting down");
}
Considering the weird animation, if you don't like it, then report to Apple. Only they can fix it. I don't think you should update the notification (if this is possible at all). Simply push a new one, and either remove the old or don't bother with it. This is what most applications do and users generally have no issues with it. It serves as a kind of history, too.

Ranging Beacons only works when app running?

I am having difficulties getting this to work for when the app is not running. I have locationManager:didRangeBeacons:inRegion: implemented and it is called when the app is running in the foreground or background, however it doesn't seem to do anything when I quit the app and lock the screen. The location services icon goes away and I never know that I entered a beacon range. Should the LocalNotification still work?
I have Location updates and Uses Bluetooth LE accessories selected in Background Modes (XCode 5) I didn't think I needed them.
Any help greatly appreciated.
-(void)watchForEvents { // this is called from application:didFinishLaunchingWithOptions
id class = NSClassFromString(#"CLBeaconRegion");
if (!class) {
return;
}
CLBeaconRegion * rflBeacon = [[CLBeaconRegion alloc] initWithProximityUUID:kBeaconUUID identifier:kBeaconString];
rflBeacon.notifyOnEntry = YES;
rflBeacon.notifyOnExit = NO;
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
[self.locationManager startRangingBeaconsInRegion:rflBeacon];
[self.locationManager startMonitoringForRegion:rflBeacon];
}
-(void)locationManager:(CLLocationManager *)manager didRangeBeacons:(NSArray *)beacons inRegion:(CLBeaconRegion *)region {
if (beacons.count == 0 || eventRanged) { // breakpoint set here for testing
return;
}
eventRanged = YES;
if (backgroundMode) { // this is set in the EnterBackground/Foreground delegate calls
UILocalNotification *notification = [[UILocalNotification alloc] init];
notification.alertBody = [NSString stringWithFormat:#"Welcome to the %# event.",region.identifier];
notification.soundName = UILocalNotificationDefaultSoundName;
[[UIApplication sharedApplication] presentLocalNotificationNow:notification];
}
// normal processing here...
}
Monitoring can launch an app that isn't running. Ranging cannot.
The key to having monitoring launch your app is to set this poorly documented flag on your CLBeaconRegion: region.notifyEntryStateOnDisplay = YES;
This can launch your app on a region transition even after completely rebooting your phone. But there are a couple of caveats:
Your app launches into the background only for a few seconds. (Try adding NSLog statements to applicationDidEnterBackground and other methods in your AppDelegate to see what is going on.)
iOS can take its own sweet time to decide you entered a CLBeaconRegion. I have seen it take up to four minutes.
As far as ranging goes, even though you can't have ranging wake up your app, you can make your app do both monitoring and ranging simultaneously. If monitoring wakes up your app and puts it into the background for a few seconds, ranging callbacks start up immediately. This gives you a chance to do any quick ranging actions while your app is still running.
EDIT: Further investigation proves that notifyEntryStateOnDisplay has no effect on background monitoring, so the above should work regardless of whether you have this flag. See this detailed explanation and discussion of delays you may experience
Code for iOS 9 to range beacons in the background, by using Location Updates:
Open Project Settings -> Capabilities -> Background Modes -> Toggle Location Updates and Uses Bluetooth LE accessories to ON.
Create a CLLocationManager, request Always monitoring authorization (don't forget to add the Application does not run in background to NO and NSLocationAlwaysUsageDescription in the app's info.plist) and set the following properties:
locationManager!.delegate = self
locationManager!.pausesLocationUpdatesAutomatically = false
locationManager!.allowsBackgroundLocationUpdates = true
Start ranging for beacons and monitoring region:
locationManager!.startMonitoringForRegion(yourBeaconRegion)
locationManager!.startRangingBeaconsInRegion(yourBeaconRegion)
locationManager!.startUpdatingLocation()
// Optionally for notifications
UIApplication.sharedApplication().registerUserNotificationSettings(
UIUserNotificationSettings(forTypes: .Alert, categories: nil))
Implement the CLLocationManagerDelegate and in your didEnterRegion send both startRangingBeaconsInRegion() and startUpdatingLocation() messages (optionally send the notification as well) and set the stopRangingBeaconsInRegion() and stopUpdatingLocation() in didExitRegion
Be aware that this solution works but it is not recommended by Apple due to battery consumption and customer privacy!
More here: https://community.estimote.com/hc/en-us/articles/203914068-Is-it-possible-to-use-beacon-ranging-in-the-background-
Here is the process you need to follow to range in background:
For any CLBeaconRegion always keep monitoring on, in background or foreground and keep notifyEntryStateOnDisplay = YES
notifyEntryStateOnDisplay calls locationManager:didDetermineState:forRegion: in background, so implement this delegate call...
...like this:
- (void)locationManager:(CLLocationManager *)manager didDetermineState:(CLRegionState)state forRegion:(CLRegion *)region{
if (state == CLRegionStateInside) {
//Start Ranging
[manager startRangingBeaconsInRegion:region];
}
else{
//Stop Ranging
[manager stopRangingBeaconsInRegion:region];
}
}
I hope this helps.
You are doing two separate operations here - 'ranging' beacons and monitoring for a region. You can monitor for a region in the background, but not range beacons.
Therefore, your implementation of locationManager:didRangeBeacons:inRegion: won't get called in the background. Instead, your call to startMonitoringForRegion will result in one / some of the following methods being called:
– locationManager:didEnterRegion:
– locationManager:didExitRegion:
– locationManager:didDetermineState:forRegion:
These will get called in the background. You can at that point trigger a local notification, as in your original code.
Your app should currently wake up if you're just wanting to be notified when you enter a beacon region. The only background restriction I know of concerns actually hosting an iBeacon on an iOS device. In that case, the app would need to be physically open in the foreground. For that situation, you'd be better off just doing the straight CoreBluetooth CBPeripheralManager implementation. That way you'd have some advertising abilities in the background.

Resources