What I need:
A predictable, reliable and reliable way to run iBeacon delegation methods, such as didDetermineState , didRangeBeacons , didEnterRegion or didExitRegion , when the application is dead and the device is connected nearby.
Current situation
I am making an application for parents to use for their children to help them close their phones at important points. The application is located in Objective-C, and it needs to maintain a constant connection with the Bluetooth device even after the life of the application.
Iโve been trying to get this to work for a long time, and I had help from a lot of SO plugins, and currently I know that I have to use iBeacon on my device to start with completed (which is the only reason I use it, I would gladly dumped it if there was another way to start the application from the completed one). To clarify, I need 2 things here, in the same iBeacon device (which I already built) and a solid BT connection. I need to connect this device, because this is the only way to send / receive commands from a BT device. I found that the delegate methods didRange or didEnter that fire in the background are unreliable at best. They don't always fire right away, and they fire just a few times, and it all dies (which I now know that this 10-second window is expected by behavior from the terminated application). I even had full whole days when I turn it on / off constantly, looking for any signs that the application has returned to life and nothing is happening ...
When the application is open, everything works fine, however, when the application is next to my beacon / bluetooth, I want it to launch a kind of temporary lock screen inside the application. I already do this part pretty well when the application is in the foreground. If the child is trying to close the application or the background, I want to answer when my BT device starts in the background after it stops (I know that the user interface will not appear, and I still need to perform a number of functions to start), then it will connect to Bluetooth and will receive some commands from the device. Sounds simple enough? Everything became messy here.
In some context: I have all the background modes added to info.plist for bluetooth and beacon, and everything works fine when the application is in the foreground ...
If an iBeacon is detected in the range, then I want to use this 10-second window to connect via BT-pairing to my mailbox and send via the command. While this is not surprising ... iBeacon range functions do not work when the application is terminated, they only start in the strangest cases. I can not predict when they are going to shoot.
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 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 current state: %ld", (long)state); [self triggerCustomLocalNotification:[NSString stringWithFormat:@"ranged beacons, application 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