CLBeaconRegion notifyEntryStateOnDisplay and UILocalNotifications - ios

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.

Related

Remove fired Location-Based notification when user exits region

I've setup (default iOS8) location-based notifications for my app.
UILocalNotification *notification = [[UILocalNotification alloc] init];
notification.regionTriggersOnce = NO;
notification.userInfo = #{ #"notification_id" : #"someID" };
notification.region = region;
notification.alertBody = alertBody;
[[UIApplication sharedApplication] scheduleLocalNotification:notification];
When a user enters the specified region, the notification is shown in the NotificationCenter correctly.
However, I want to remove that notification message when the user exits that region, because it makes little sense for the user to go home and look at the notification center until they recieve a message which looks like so:
"You are at XXXXX!"
Has anyone tried something similar? The documentation is unclear on how this can be done.
CLRegion object has special properties for that: notifyOnEntry and notifyOnExit.
Everything that you need is update code in this way:
CLRegion *region = .... // Configure region here
region.notifyOnEntry = YES;
region.notifyOnExit = NO;
UILocalNotification *notification = [[UILocalNotification alloc] init];
notification.regionTriggersOnce = NO;
notification.userInfo = #{ #"notification_id" : #"someID" };
notification.region = region;
notification.alertBody = alertBody;
[[UIApplication sharedApplication] scheduleLocalNotification:notification];
Here is Apple documentation that explains it:
#property(nonatomic, copy) CLRegion *region
Assigning a value to this
property causes the local notification to be delivered when the user
crosses the region’s boundary.
The region object itself defines
whether the notification is triggered when the user enters or exits
the region.
I was really tired yesterday and couldn't complete my answer in time.
Based on some answers here I have only a clue what you have to do. I did not tried it myself (geofencing is a really a pain to test, I know that because I'm working on a geofencing project atm.), I also never had to delete a delivered notification from the Notification Center before.
I assume your application will not terminate due the whole process. So we won't use startMonitoringForRegion function here.
Answer written in Swift 2.0 (It's not that hard to translate it to ObjC).
func createAndRegisterSomeNotificationSomewhere() {
let region = CLCircularRegion(center: someCoordinates, radius: someRadius, identifier: someIdentifier)
region.notifyOnEntry = true
region.notifyOnExit = true
let locationNotification = UILocalNotification()
locationNotification.alertBody = "someAlertBody"
locationNotification.userInfo = ["notification_id" : "someID"]
locationNotification.regionTriggersOnce = false
locationNotification.region = region // remember 'presentLocalNotificationNow' will not work if this value is set
UIApplication.sharedApplication().scheduleLocalNotification(locationNotification)
}
/* CLLocationManagerDelegate provides two function */
// func locationManager(manager: CLLocationManager, didEnterRegion region: CLRegion)
// func locationManager(manager: CLLocationManager, didExitRegion region: CLRegion)
/* If I'm not mistaken they are only called for monitored regions and not location based local notifications */
/* I mean you will have to use something like: self.locationManager.startMonitoringForRegion(someCircularRegion) */
/* Correct me if I'm wrong. So consider to rebuild the following logic to ease everything if you want to monitor regions. */
/* Now when you receive your location notification */
func application(application: UIApplication, didReceiveLocalNotification notification: UILocalNotification) {
if let region = notification.region {
self.locationManager.requestStateForRegion(region)
/* based on other answers this will remove your noticaiton from NC and cancel from showing it anywhere */
application.cancelLocalNotification(notification)
/* but we need this notification still be scheduled because 'region.notifyOnExit = true' should fire it again later */
application.scheduleLocalNotification(notification)
}
}
func locationManager(manager: CLLocationManager, didDetermineState state: CLRegionState, forRegion region: CLRegion) {
/* this is not the best solution, because it adds some latency to the dilivery code and CLRegionState can also be Unknown sometimes */
/* I'd go with the two functions above if I only had up to 20 regions to monitor (max region limit per App, read CLLocationManager docs) */
/* the mechanics would be more clear and save */
switch state {
case .Inside:
/* create a new noticiation with the same cpecs as the cancled notification but this time withot the region */
let sameNotificationAsAbove = UILocalNotification()
/* if you really need to know your IDs inside userInfo so create some good mechanics to pass these before canceling */
/* at least I would save the identifier of the region iside the noticiation */
/* save the notification somewhere to delete it later from NC */
self.someArrayToSaveDeliveredNotifications.append(sameNotificationAsAbove)
/* fire the notification */
UIApplication.sharedApplication().presentLocalNotificationNow(sameNotificationAsAbove)
case default:
/* if it is true that notication inside NC can be deleted just by calling 'cancelLocalNotification' function */
/* so find your notification inside someArrayToSaveDeliveredNotifications bases on the region.identier which you saved inside userInfo */
let notificationToCancel = self.getNotificationForIdentifier(region.identifier)
UIApplication.sharedApplication().cancelLocalNotification(notificationToCancel)
/* this should delete your notification from NC based on other answers */
}
}
This is some kind of pseudo mechanics I would build if I had to, so if anything is wrong or not correct I would appreciate to hear your feedback. :)
This will clear all of the app's notifications from Notification Center.
[[UIApplication sharedApplication] setApplicationIconBadgeNumber: 0];
[[UIApplication sharedApplication] cancelAllLocalNotifications];
It appears that you can clear a specific notification if you hold onto the UILocalNotification object. Using your the notification object you created in your example above you can call
[[UIApplication sharedApplication] cancelLocalNotification:notification];
to clear the notification.
It looks like very simple solution. follow the below steps it will help you.
Make one background service which will work continue in the background to check your location.
When you are entering in some region fire local notification which is already done by you. good work.
But when you are leaving that region (check background service with enter location details like now location is matching with the location details when user entered in that region). fire the new local notification with empty data. and than it will clear the notification from the notification window also.
You should trigger a background method which should only be called when the device is going out of the location. This can be achieved by creating layer for region and trigger immediately when it is coming out of boundary.
In the method, you may either clear all the notification of the respective app by
[[UIApplication sharedApplication] cancelLocalNotification:notification];
or may clear only specific notification by calling
UIApplication* application = [UIApplication sharedApplication];
NSArray* scheduledNotifications = [NSArray arrayWithArray:application.scheduledLocalNotifications];
application.scheduledLocalNotifications = scheduledNotifications;
You will receive all the valid notification available for the particular app. Delete the specific notification for the specific region.
CLLocationManagerDelegate delegate has a set of methods that get triggered base on the device location. Such as;
-(void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region
-(void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region
-(void)locationManager:(CLLocationManager *)manager didRangeBeacons:(NSArray *)beacons inRegion:(CLBeaconRegion *)region
In your case what you have to do is when the following callback get triggered;
-(void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region
remove your notification from the NotificationCenter

Multipeer connectivity with one device having app running in background

I would like to connect 2 devices using multipeer connectivity framework where one of those devices is running the app in the background, just like Firechat does (I can't confirm this is working, I have installed it on an iPhone 5S and 4, but they just can't find each other - but I have read somewhere this works).
What's the best way to achieve this?
I'm using the following two methods from an example code:
-(void)setupPeerAndSessionWithDisplayName:(NSString *)displayName{
_peerID = [[MCPeerID alloc] initWithDisplayName:displayName];
_session = [[MCSession alloc] initWithPeer:_peerID];
_session.delegate = self;
}
-(void)setupMCBrowser{
_browser = [[MCBrowserViewController alloc] initWithServiceType:#"chat-files" session:_session];
}
-(void)advertiseSelf:(BOOL)shouldAdvertise{
if (shouldAdvertise) {
_advertiser = [[MCAdvertiserAssistant alloc] initWithServiceType:#"chat-files"
discoveryInfo:nil
session:_session];
[_advertiser start];
}
else{
[_advertiser stop];
_advertiser = nil;
}
}
When I'm running the app in the foreground, it finds the other device flawlessly and also connects. But if I put one of the apps into background, the background-app-device is no longer visible.
I have already read this: Can I have a multipeer connectivity session run in the background? - But I can't believe this is not possible without any workaround.
FYI, my app is also using background location updates, if that's relevant...
Thanks!
EDIT:
Is there some other way of doing this?? Basically I just want to send a message to the other device (to the one who's app is running in the background) - since Multipeer connectivity doesn't work in the background, can I connect via bluetooth directly for example?
Apple have confirmed that "Multipeer Connectivity does not function in the background". The reason is that if your app is suspended then its socket resources can be reclaimed and everything falls apart.
However, it is possible to monitor iBeacons when in the background. Essentially, you set your app up to monitor proximity to a beacon with a specific id. Then, your app that is in the foreground becomes a beacon with that same id. This causes the following app delegate method to be called on the app in the background:
- (void)locationManager:(CLLocationManager *)manager didDetermineState:(CLRegionState)state forRegion:(CLRegion *)region {
UILocalNotification *notification = [[UILocalNotification alloc] init];
if(state == CLRegionStateInside) {
notification.alertBody = NSLocalizedString(#"A nearby app would like to connect", #"");
} else {
return;
}
[[UIApplication sharedApplication] presentLocalNotificationNow:notification];
}
Here you issue a local notification that, when tapped on by the user, will bring your app into the foreground, at which point you can start advertising for a peer-to-peer connection.

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!

CLLocationManager, didEnterRegion and UILocalNotification

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.

iOS: Push notifications, UIApplicationStateInactive and fast app switching

According to the Apple Docs, in order to find out if a user tapped on your push notification you are supposed to check the applicationState in application:didReceiveRemoteNotification:
If the value is UIApplicationStateInactive, the user tapped the action button; if the value is UIApplicationStateActive, the application was frontmost when it received the notification.
I have found that this is not always true. For example:
Double-tap the home button to reveal the system tray and enter "fast app switching mode", your application slides up to reveal other running applications and your app is put into the inactive state (even though it's still mostyle visible). If you receive a push notification in this mode your app delegate will still receive the application:didReceiveRemoteNotification: and at this point your applicationState is UIApplicationStateActive. According to the docs you should treat it like the user tapped the alert... but in this case they didn't. Not only that, the user didn't even see the push notification (possibly because the top of your application is cut off in this mode).
Does anyone know of a way to detect being in 'fast app switching mode' or handle the notification correctly?
I was able to fix it myself with some nifty checks...
Essentially the key to this whole thing is
-(void)applicationDidEnterBackground:(UIApplication *)application;
This method isn't called when you enter fast app switching (or control center) so you need to setup a check based on it.
#property BOOL isInBackground;
#property (nonatomic, retain) NSMutableArray *queuedNotifications;
And when you receive a notification...
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
UIApplicationState appState = application.applicationState;
// Check if we're in this special state. If so, queue the message up
if (appState == UIApplicationStateInactive && !self.isInBackground) {
// This is a special case in which we're in fast app switching or control center
if (!self.queuedNotifications) {
self.queuedNotifications = [NSMutableArray array];
}
// Queue this to show when we come back
[self.queuedNotifications addObject:userInfo];
}
}
And then when we come back...
- (void)applicationDidBecomeActive:(UIApplication *)application {
application.applicationIconBadgeNumber = 0;
if (!self.isInBackground) {
// Show your notifications here
// Then make sure to reset your array of queued notifications
self.queuedNotifications = [NSMutableArray array];
}
}
One more thing you may want to do is check for this special case of going to fast app switching and the user going somewhere else. I do this just before setting the isInBackground BOOL. I choose to send them as local notifications
-(void)applicationDidEnterBackground:(UIApplication *)application {
for (NSDictionary *eachNotification in self.queuedNotifications) {
UILocalNotification *notification = [self convertUserInfoToLocalNotification:eachNotification];
[[UIApplication sharedApplication] scheduleLocalNotification:notification];
}
self.queuedNotifications = [NSMutableArray array];
self.isInBackground = YES;
}

Resources