Что мне нужно:
Предсказуемый, надежный и надежный способ запуска методов делегирования iBeacon, таких как didDetermineState
, didRangeBeacons
, didEnterRegion
или didExitRegion
, когда приложение не работает, а устройство подключено и находится поблизости.
Текущая ситуация
Я делаю приложение, которое родители могут использовать для своих детей, чтобы помочь им выключить телефоны в важные моменты. Приложение находится в Objective-C, и ему необходимо поддерживать постоянное соединение с устройством Bluetooth даже после окончания срока службы приложения.
Я долгое время пытался заставить это работать, и мне помогли многие S.O. плакаты, и в настоящее время я знаю, что я должен использовать iBeacon на своем устройстве для запуска с завершенного (это единственная причина, по которой я его использую, я бы с удовольствием сбросил его, если бы был другой способ запуска приложения с завершенного). Чтобы уточнить, мне нужны 2 вещи в одном устройстве (которое я уже построил) iBeacon и надежное соединение BT. Мне нужно сопряжение этого устройства, потому что это единственный способ отправлять/получать команды с устройства BT. Я обнаружил, что методы делегата didRange
или didEnter
, которые срабатывают в фоновом режиме, в лучшем случае ненадежны. Они не всегда срабатывают сразу, они срабатывают только несколько раз, и все это умирает (теперь я знаю, что это 10-секундное окно является ожидаемым поведением от завершенного приложения). У меня даже были полные целые дни, когда я постоянно подключал/отключал его в поисках любого признака того, что приложение вернулось к жизни, и ничего не происходит...
Когда приложение открыто, все работает нормально, однако, когда приложение находится рядом с моим маяком / bluetooth, я хочу, чтобы оно запускало своего рода импровизированный экран блокировки внутри приложения. Я уже делаю эту часть довольно хорошо, когда приложение находится на переднем плане. Если ребенок пытается закрыть приложение или фон, я хочу отреагировать, запустив мое устройство BT в фоновом режиме после его завершения (я знаю, что пользовательский интерфейс не появится, и это нормально, мне просто нужно несколько функций для запуска) . Затем он подключится к Bluetooth и получит некоторые команды от устройства. Звучит достаточно просто, а? Здесь все запуталось.
Некоторый контекст: я добавил все фоновые режимы в info.plist для Bluetooth и маяка, и все работает нормально, когда приложение находится на переднем плане...
Если iBeacon обнаружен в пределах досягаемости, я хочу использовать это 10-секундное окно для подключения через сопряжение BT к моему блоку и отправки команды. Пока что это шатко... Функции ранжирования iBeacon не срабатывают, когда приложение завершается, они срабатывают только в самых странных случаях использования. Кажется, я не могу предсказать, когда они собираются стрелять.
Мой код
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