IOS iBeacon / Bluetooth when the application is dead and gone

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 
+2
source share
1 answer

I built a similar system for iOS that uses iBeacon transmissions to wake up in the background, and then connect to bluetooth LE to exchange data. Be sure that all this is possible, itโ€™s just hard to work and harder to debug.

A few tips on this with Bluetooth LE connectivity:

  • The beacon ranking functions will not work when the application is killed, unless you also monitor the beacons and get a didEnter or didExit , which will restart the application in the background within 10 seconds you describe. Again, this will only happen if you move from region to region or vice versa. This is difficult to verify because you cannot understand that CoreLocation considers you to be โ€œin the regionโ€ when you kill the application, but you do not receive an awakening event to detect the beacon.

  • To receive bluetooth events in the background, you need to make sure your Info.plist announces this:

     <key>UIBackgroundModes</key> <array> <string>bluetooth-central</string> </array> 

    If not, you absolutely will not get callbacks in didDiscoverPeripheral in the background.

  • You will need to start scanning Bluetooth when the application starts and connect when you get a response to func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber)

  • Keep a copy of the peripheral instance on top, because you only get one callback in the background to be detected from each unique Bluetooth device. If your connection fails, you can try again with the same instance of the peripheral object.

  • To debug a restart from a dead state, I add a lot of NSLog statements (I add the ability to turn them on and off in the code), and then look for them in Xcode Windows -> Devices -> My iPhone panel, where you can expand the small arrow at the bottom screen to display logs for all applications on the device. You will absolutely see the logs here for your application if it is restarted from a killed state.

+2
source

Source: https://habr.com/ru/post/1270074/


All Articles