What I need:
A predictable, solid and reliable way of launching iBeacon delegate methods such as didDetermineState
, didRangeBeacons
, didEnterRegion
or didExitRegion
when the app is dead and the device is plugged in and nearby.
The Current Situation
I am making an app for parents to use for their kids to help them shut down their phones during important times. The app is in Objective-C and it needs to maintain a persistent connection to a bluetooth device even after the life of the application.
I have been trying for a long time to get this to work and I have had help from a lot of S.O. posters and currently I know that I must use iBeacon in my device to launch from terminated (that's the only reason I use it, I would gladly dump it if there was another way to launch an app from terminated). To clarify, I need 2 things here in the same device (which I have already built) iBeacon and a solid BT connection. I need this device connection pairing because this is the only way to send/receive commands from the BT device. What I have discovered is that the didRange
or didEnter
delegate methods that fire in the background are unreliable at best. They don't always fire right away and they only fire a few times and the whole thing dies (which I now know this 10 second window is expected behaviour from a terminated app). I have even had full entire days where I plug/unplug it constantly looking for any sign that the app has come back to life and nothing happens...
When the app is open, things work fine, however when the app is nearby to my beacon/bluetooth I want it to launch a sort of makeshift lock screen inside the app. I am already doing this part fairly well when the app is in the foreground. If a kid tries to close the app or background it I want to respond by having my BT device launch into the background once it's terminated (I know the UI won't come up and that's fine I just need a series of functions to fire). It will then connect to bluetooth and receive some commands from the device. Sounds simple enough eh? Here's were things get messy.
Some context: I have all background modes added in info.plist for bluetooth and beacon and everything works fine when the app is in foreground...
If the iBeacon is detected in range, I then want to use that 10 second window to connect via BT pairing to my box and send through a command. So far, this is wonky... The iBeacon ranging functions do not fire when the app is terminated they only fire on the strangest of use cases. I cannot seem to predict when they are going to fire.
My Code
ibeaconManager.h
@interface IbeaconManager : NSObject
@property (nonatomic) BOOL waitingForDeviceCommand;
@property (nonatomic, strong) NSTimer *deviceCommandTimer;
+ (IbeaconManager *) sharedInstance;
- (void)startMonitoring;
- (void)stopMonitoring;
- (void)timedLock:(NSTimer *)timer;
@end
ibeaconManager.m
@interface IbeaconManager () <CLLocationManagerDelegate>
@property (nonatomic, strong) BluetoothMgr *btManager;
@property (nonatomic, strong) CLLocationManager *locationManager;
@property (nonatomic, strong) CLBeaconRegion *region;
@property (nonatomic) BOOL connectedToDevice;
@end
NSString *const PROXMITY_UUID = @"00000000-1111-2222-3333-AAAAAAAAAAAA";
NSString *const BEACON_REGION = @"MY_CUSTOM_REGION";
const int REGION_MINOR = 0;
const int REGION_MAJOR = 0;
@implementation IbeaconManager
+ (IbeaconManager *) sharedInstance {
static IbeaconManager *_sharedInstance = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_sharedInstance = [[IbeaconManager alloc] init];
});
return _sharedInstance;
}
- (id)init {
self = [super init];
if(self) {
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
[self.locationManager requestAlwaysAuthorization];
self.connectedToDevice = NO;
self.waitingForDeviceCommand = NO;
self.region = [[CLBeaconRegion alloc] initWithProximityUUID:[[NSUUID alloc] initWithUUIDString:PROXMITY_UUID]
major:REGION_MAJOR
minor:REGION_MINOR
identifier:BEACON_REGION];
self.region.notifyEntryStateOnDisplay = YES;
self.region.notifyOnEntry = YES;
self.region.notifyOnExit = YES;
}
return self;
}
- (void)startMonitoring {
if(self.region != nil) {
NSLog(@"**** started monitoring with beacon region **** : %@", self.region);
[self.locationManager startMonitoringForRegion:self.region];
[self.locationManager startRangingBeaconsInRegion:self.region];
}
}
- (void)stopMonitoring {
NSLog(@"*** stopMonitoring");
if(self.region != nil) {
[self.locationManager stopMonitoringForRegion:self.region];
[self.locationManager stopRangingBeaconsInRegion:self.region];
}
}
- (void)triggerCustomLocalNotification:(NSString *)alertBody {
UILocalNotification *localNotification = [[UILocalNotification alloc] init];
localNotification.alertBody = alertBody;
[[UIApplication sharedApplication] presentLocalNotificationNow:localNotification];
}
#pragma mark - CLLocationManager delegate methods
- (void)locationManager:(CLLocationManager *)manager
didDetermineState:(CLRegionState)state
forRegion:(CLRegion *)region {
NSLog(@"did determine state STATE: %ld", (long)state);
NSLog(@"did determine state region: %@", region);
[self triggerCustomLocalNotification:@"made it into the did determine state method"];
NSUInteger appState = [[UIApplication sharedApplication] applicationState];
NSLog(@"application's current state: %ld", (long)appState);
if(appState == UIApplicationStateBackground || appState == UIApplicationStateInactive) {
NSString *notificationText = @"Did range beacons... The app is";
NSString *notificationStateText = (appState == UIApplicationStateInactive) ? @"inactive" : @"backgrounded";
NSString *notificationString = [NSString stringWithFormat:@"%@ %@", notificationText, notificationStateText];
NSUserDefaults *userDefaults = [[NSUserDefaults alloc] init];
bool isAppLockScreenShowing = [userDefaults boolForKey:@"isAppLockScreenShowing"];
if(!isAppLockScreenShowing && !self.waitingForDeviceCommand) {
self.waitingForDeviceCommand = YES;
self.deviceCommandTimer = [NSTimer scheduledTimerWithTimeInterval:2.0
target:self
selector:@selector(timedLock:)
userInfo:notificationString
repeats:NO];
}
} else if(appState == UIApplicationStateActive) {
if(region != nil) {
if(state == CLRegionStateInside) {
NSLog(@"locationManager didDetermineState INSIDE for %@", region.identifier);
[self triggerCustomLocalNotification:@"locationManager didDetermineState INSIDE"];
} else if(state == CLRegionStateOutside) {
NSLog(@"locationManager didDetermineState OUTSIDE for %@", region.identifier);
[self triggerCustomLocalNotification:@"locationManager didDetermineState OUTSIDE"];
} else {
NSLog(@"locationManager didDetermineState OTHER for %@", region.identifier);
}
}
//Upon re-entry, remove timer
if(self.deviceCommandTimer != nil) {
[self.deviceCommandTimer invalidate];
self.deviceCommandTimer = nil;
}
}
}
- (void)locationManager:(CLLocationManager *)manager
didRangeBeacons:(NSArray *)beacons
inRegion:(CLBeaconRegion *)region {
NSLog(@"Did range some beacons");
NSUInteger state = [[UIApplication sharedApplication] applicationState];
NSString *notificationStateText = (state == UIApplicationStateInactive) ? @"inactive" : @"backgrounded";
NSLog(@"application's current state: %ld", (long)state);
[self triggerCustomLocalNotification:[NSString stringWithFormat:@"ranged beacons, application's current state: %@", notificationStateText]];
if(state == UIApplicationStateBackground || state == UIApplicationStateInactive) {
NSString *notificationText = @"Did range beacons... The app is";
NSString *notificationString = [NSString stringWithFormat:@"%@ %@", notificationText, notificationStateText];
NSUserDefaults *userDefaults = [[NSUserDefaults alloc] init];
bool isAppLockScreenShowing = [userDefaults boolForKey:@"isAppLockScreenShowing"];
if(!isAppLockScreenShowing && !self.waitingForDeviceCommand) {
self.waitingForDeviceCommand = YES;
self.deviceCommandTimer = [NSTimer scheduledTimerWithTimeInterval:2.0
target:self
selector:@selector(timedLock:)
userInfo:notificationString
repeats:NO];
}
} else if(state == UIApplicationStateActive) {
if(self.deviceCommandTimer != nil) {
[self.deviceCommandTimer invalidate];
self.deviceCommandTimer = nil;
}
}
}
- (void)timedLock:(NSTimer *)timer {
self.btManager = [BluetoothMgr sharedInstance];
[self.btManager sendCodeToBTDevice:@"magiccommand"
characteristic:self.btManager.lockCharacteristic];
[self triggerCustomLocalNotification:[timer userInfo]];
self.waitingForDeviceCommand = NO;
}
- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region {
NSLog(@"Did Enter Region: %@", region);
[self triggerCustomLocalNotification:[NSString stringWithFormat:@"Did enter region: %@", region.identifier]];
}
- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region {
NSLog(@"Did Exit Region: %@", region);
[self triggerCustomLocalNotification:[NSString stringWithFormat:@"Did exit region: %@", region.identifier]];
//Upon exit, remove timer
if(self.deviceCommandTimer != nil) {
[self.deviceCommandTimer invalidate];
self.deviceCommandTimer = nil;
}
}
- (void)locationManager:(CLLocationManager *)manager monitoringDidFailForRegion:(CLRegion *)region withError:(NSError *)error {
NSLog(@"monitoringDidFailForRegion EPIC FAIL for region %@ withError %@", region.identifier, error.localizedDescription);
}
@end