Подключение iOS iBeacon / Bluetooth, когда приложение мертво и исчезло

Что мне нужно:

Предсказуемый, надежный и надежный способ запуска методов делегирования 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

person GoreDefex    schedule 17.07.2017    source источник


Ответы (1)


Я создал аналогичную систему для iOS, которая использует передачи iBeacon для пробуждения в фоновом режиме, а затем подключается к Bluetooth LE для обмена данными. Будьте уверены, что все это возможно, просто сложно заставить работать и еще сложнее отлаживать.

Несколько советов, как сделать это с подключением Bluetooth LE:

  • Функции ранжирования маяка не сработают, когда приложение будет уничтожено, если вы также отслеживаете маяки и получаете переход didEnter или didExit, который повторно запускает приложение в фоновом режиме на 10 секунд, как вы описываете. Опять же, это произойдет только в том случае, если вы перейдете из региона в регион или наоборот. Это сложно проверить, потому что вы можете не осознавать, что CoreLocation считает, что вы находитесь «в регионе», когда вы закрываете приложение, но вы не получите событие пробуждения для обнаружения маяка.

  • Чтобы получать события Bluetooth в фоновом режиме, вам нужно убедиться, что ваш Info.plist объявляет это:

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

    Если этого нет, вы абсолютно не будете получать обратные вызовы на didDiscoverPeripheral в фоновом режиме.

  • Вам нужно будет начать сканирование Bluetooth при запуске приложения и подключиться, когда вы получите обратный вызов на func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber).

  • Сохраните копию экземпляра peripheral из приведенного выше, потому что вы получаете только один обратный вызов в фоновом режиме для обнаружения с каждого уникального устройства Bluetooth. В случае сбоя соединения вы можете повторить попытку с тем же экземпляром объекта peripheral.

  • Чтобы отладить повторный запуск из убитого состояния, я добавляю множество операторов NSLog (я добавляю возможность включать и выключать их в коде), а затем ищу их на панели XCode Windows -> Devices -> My iPhone, где вы можете развернуть маленькую стрелку в нижней части экрана, чтобы отобразить журналы для всех приложений на устройстве. Вы обязательно увидите здесь журналы для своего приложения, если оно будет перезапущено из убитого состояния.

person davidgyoung    schedule 17.07.2017
comment
Рад, что у тебя получилось. Если вы можете предложить какие-либо моменты о том, что имело значение для вас, пожалуйста, прокомментируйте, поскольку это может помочь другим с аналогичными проблемами. - person davidgyoung; 18.07.2017
comment
хорошо, так что, возможно, на днях, когда я сказал, что это работает, это было скорее случайностью. Сегодня я провел более 4 часов, подключая/отключая устройство, и я не получил ни одного журнала или уведомления для любого из следующих методов: didDetermineState, didRangeBeacons, didEnterRegion, didExitRegion или monitoringDidFailForRegion - person GoreDefex; 20.07.2017
comment
понятия не имею, как поступить в этом вопросе. У приложения нет проблем с обнаружением iBeacon, когда я запускаю приложение в фоновом режиме, метод didDetermineState срабатывает почти каждую секунду, что довольно надежно. Когда я завершаю приложение, мне нужно, чтобы какой-то метод iBeacon был очень надежно обнаружен. Кроме того, мне нужно то же самое, когда приложение было прекращено в течение нескольких дней или запускается во второй раз. - person GoreDefex; 20.07.2017
comment
обновил мой исходный вопрос новой информацией и добавил мой код, чтобы мне было легче помочь - person GoreDefex; 20.07.2017
comment
Вы не одиноки здесь. Тестирование жесткое. Если вы не получаете никаких триггеров мониторинга, я подозреваю, что вы можете быть внутри региона. Транзиты наружу в фоновом режиме могут быть довольно медленными — занимая 15-30 минут, поскольку они зависят от оппортунистического сканирования другими компонентами, работающими в ОС. Я бы вывел приложение на передний план, убедился, что вы находитесь за пределами региона, затем поместил приложение в фоновый режим (или убил его), а затем включил маяк, чтобы попытаться получить триггер. Еще один совет: если вы когда-нибудь перезагрузите свой телефон, не ждите никаких триггеров в течение 5 минут. CoreLocation не запускается полностью быстро при загрузке iOS. - person davidgyoung; 21.07.2017
comment
Сегодня, по какой-то причине, все кажется другим. Функции теперь срабатывают более надежно! Код вообще не изменился, единственное, что изменилось, это то, что я уходил домой с работы на день и возвращался на следующий день. Это заставило меня задуматься, возможно, это происходит из-за того, что телефон не двигается (широта/долгота не изменились), пока я разрабатываю? Я начал второй вопрос об этом, чтобы попытаться еще больше сузить это. Если у вас есть минутка, возможно, вы можете проверить это? stackoverflow.com/questions/45241033/ - person GoreDefex; 21.07.2017