1

I would like to use the iBeacon Feature in an iPad App to act as a beacon transmitter and an iphone App to receive the beacons. I was able to build both apps accordingly but now I ran into some strange problems:

iBeacon Transmitter the iPad app acts as the transmitter of the beacon signal. I implemented an action sheet for selecting a beacon ID I would like to transmit. This is the code for that:

#import "BeaconAdvertisingService.h"
@import CoreBluetooth;

NSString *const kBeaconIdentifier = @"identifier";

@interface BeaconAdvertisingService () <CBPeripheralManagerDelegate>
@property (nonatomic, readwrite, getter = isAdvertising) BOOL advertising;
@end

@implementation BeaconAdvertisingService {
    CBPeripheralManager *_peripheralManager;
}

+ (BeaconAdvertisingService *)sharedInstance {
    static BeaconAdvertisingService *sharedInstance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}

- (instancetype)init {
    self = [super init];
    if (!self) {
        return nil;
    }
    _peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];

    return self;
}

- (BOOL)bluetoothStateValid:(NSError **)error {
    BOOL bluetoothStateValid = YES;
    switch (_peripheralManager.state) {
        case CBPeripheralManagerStatePoweredOff:
            if (error != NULL) {
                *error = [NSError errorWithDomain:@"identifier.bluetoothState"
                                             code:CBPeripheralManagerStatePoweredOff
                                         userInfo:@{@"message": @"You must turn Bluetooth on in order to use the beacon feature"}];
            }
            bluetoothStateValid = NO;
            break;
        case CBPeripheralManagerStateResetting:
            if (error != NULL) {
                *error = [NSError errorWithDomain:@"identifier.bluetoothState"
                                             code:CBPeripheralManagerStateResetting
                                         userInfo:@{@"message" : @"Bluetooth is not available at this time, please try again in a moment."}];
            }
            bluetoothStateValid = NO;
            break;
        case CBPeripheralManagerStateUnauthorized:
            if (error != NULL) {
                *error = [NSError errorWithDomain:@"identifier.bluetoothState"
                                             code:CBPeripheralManagerStateUnauthorized
                                         userInfo:@{@"message": @"This application is not authorized to use Bluetooth, verify your settings or check with your device's administration"}];
            }
            bluetoothStateValid = NO;
            break;
        case CBPeripheralManagerStateUnknown:
            if (error != NULL) {
                *error = [NSError errorWithDomain:@"identifier.bluetoothState"
                                             code:CBPeripheralManagerStateUnknown
                                         userInfo:@{@"message": @"Bluetooth is not available at this time, please try again in a moment"}];
            }
            bluetoothStateValid = NO;
            break;
        case CBPeripheralManagerStateUnsupported:
            if (error != NULL) {
                *error = [NSError errorWithDomain:@"identifier.blueetoothState"
                                             code:CBPeripheralManagerStateUnsupported
                                         userInfo:@{@"message": @"Your device does not support bluetooth. You will not be able to use the beacon feature"}];
            }
            bluetoothStateValid = NO;
            break;
        case CBPeripheralManagerStatePoweredOn:
            bluetoothStateValid = YES;
            break;
    }
    return bluetoothStateValid;
}

- (void)startAdvertisingUUID:(NSUUID *)uuid major:(CLBeaconMajorValue)major minor:(CLBeaconMinorValue)minor {
    NSError *bluetoothStateError = nil;
    if (![self bluetoothStateValid:&bluetoothStateError]) {
        NSString *title = @"Bluetooth Issue";
        NSString *message = bluetoothStateError.userInfo[@"message"];

        [[[UIAlertView alloc] initWithTitle:title
                                    message:message
                                   delegate:nil
                          cancelButtonTitle:@"OK"
                          otherButtonTitles:nil] show];
        return;
    }

    CLBeaconRegion *region;
    if (uuid && major && minor) {
        region = [[CLBeaconRegion alloc] initWithProximityUUID:uuid major:major minor:minor identifier:kBeaconIdentifier];
    } else if (uuid && major) {
        region = [[CLBeaconRegion alloc] initWithProximityUUID:uuid major:major identifier:kBeaconIdentifier];
    } else if (uuid) {
        region = [[CLBeaconRegion alloc] initWithProximityUUID:uuid identifier:kBeaconIdentifier];
    } else {
        [NSException raise:@"You must at least provide a UUID to start advertising" format:nil];
    }

    NSDictionary *peripheralData = [region peripheralDataWithMeasuredPower:nil];
    [_peripheralManager startAdvertising:peripheralData];
}

- (void)stopAdvertising {
    [_peripheralManager stopAdvertising];
    self.advertising = NO;
}

- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral {
    NSError *bluetoothStateError = nil;
    if (![self bluetoothStateValid: &bluetoothStateError]) {
        dispatch_async(dispatch_get_main_queue(), ^{
            UIAlertView *bluetoothIssueAlert = [[UIAlertView alloc] initWithTitle:@"Bluetooth Problem"
                                                                          message:bluetoothStateError.userInfo[@"message"]
                                                                         delegate:nil
                                                                cancelButtonTitle:@"OK"
                                                                 otherButtonTitles:nil];
            [bluetoothIssueAlert show];
        });
    }
}

- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral error:(NSError *)error {
    dispatch_async(dispatch_get_main_queue(), ^{
        if (error) {
            [[[UIAlertView alloc] initWithTitle:@"Cannot Advertise Beacon"
                                        message:@"There was an issue starting the advertisement of the beacon"
                                       delegate:nil
                              cancelButtonTitle:@"OK"
                              otherButtonTitles:nil] show];
        } else {
            NSLog(@"Advertising");
            self.advertising = YES;
        }
    });
}

As far as I can see, the transmitting works perfectly fine...

The iphone App I would like to respond to the received signal ID should throw an local notification as soon as it received the ID. This works perfectly fine on the first run. I can select every of the 3 beacons in the ipad action sheet to throw this notification on the iphone. But when I reselect the first beacon for example nothing happens any more. For the purpose of the app it would be crucial the app responds every time it receives the beacon. I setup the iphone code as follows:

#import "BeaconMonitoringService.h"
#import "LocationManagerService.h"

@implementation BeaconMonitoringService {
    CLLocationManager *_locationManager;
}

+ (BeaconMonitoringService *)sharedInstance {
    static dispatch_once_t onceToken;
    static BeaconMonitoringService *_sharedInstance;
    dispatch_once(&onceToken, ^{
        _sharedInstance = [[self alloc] init];
    });
    return _sharedInstance;
}

- (instancetype)init {
    self = [super init];
    if (!self) {
        return nil;
    }
    _locationManager = [[LocationManagerService sharedInstance] getLocationManager];
    return self;
}

- (void)startMonitoringBeaconWithUUID:(NSUUID *)uuid major:(CLBeaconMajorValue)major minor:(CLBeaconMinorValue)minor identifier:(NSString *)identifier onEntry:(BOOL)entry onExit:(BOOL)exit {
    CLBeaconRegion *region = [[CLBeaconRegion alloc] initWithProximityUUID:uuid major:major minor:minor identifier:identifier];
    region.notifyOnEntry = entry;
    region.notifyOnExit = exit;
    region.notifyEntryStateOnDisplay = YES;
    [_locationManager startMonitoringForRegion:region];
}

- (void)stopMonitoringAllRegions {
    for (CLRegion *region in _locationManager.monitoredRegions) {
        [_locationManager stopMonitoringForRegion:region];
    }
}

@end

The location manager throws its delegate calls accordingly and is implemented by me in a locationmanagerservice.

- (void)locationManager:(CLLocationManager *)manager didDetermineState:(CLRegionState)state forRegion:(CLRegion *)region {
    if ([region isKindOfClass:[CLBeaconRegion class]]) {
        CLBeaconRegion *beaconRegion = (CLBeaconRegion *)region;
        Beacon *beacon = [[BeaconDetailService sharedService] beaconWithUUID:beaconRegion.proximityUUID];
        if (beacon) {
            NSDictionary *userInfo = @{@"beacon": beacon, @"state": @(state)};
            [[NSNotificationCenter defaultCenter] postNotificationName:@"DidDetermineRegionState" object:self userInfo:userInfo];
        }

        NSLog(@"Call DidDetermine");
    }
}

- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region {
    dispatch_async(dispatch_get_main_queue(), ^{
        if ([region isKindOfClass:[CLBeaconRegion class]]) {
            CLBeaconRegion *beaconRegion = (CLBeaconRegion *)region;
            Beacon *beacon = [[BeaconDetailService sharedService] beaconWithUUID:beaconRegion.proximityUUID];
            if (beacon) {
                UILocalNotification *notification = [[UILocalNotification alloc] init];
                notification.userInfo = @{@"uuid": beacon.uuid.UUIDString};
                notification.alertBody = [NSString stringWithFormat:@"Test Beacon %@", beacon.name];
                notification.soundName = @"Default";
                [[UIApplication sharedApplication] presentLocalNotificationNow:notification];
                [[NSNotificationCenter defaultCenter] postNotificationName:@"DidEnterRegion" object:self userInfo:@{@"beacon": beacon}];

                NSLog(@"Call DidEnter");
            }
        }
    });
}

- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region {
    dispatch_async(dispatch_get_main_queue(), ^{
        if ([region isKindOfClass:[CLBeaconRegion class]]) {
            CLBeaconRegion *beaconRegion = (CLBeaconRegion *)region;
            Beacon *beacon = [[BeaconDetailService sharedService] beaconWithUUID:beaconRegion.proximityUUID];
            if (beacon) {
                UILocalNotification *notification = [[UILocalNotification alloc] init];
                notification.alertBody = [NSString stringWithFormat:@"Test %@", beacon.name];
                [[UIApplication sharedApplication] presentLocalNotificationNow:notification];
                [[NSNotificationCenter defaultCenter] postNotificationName:@"DidExitRegion" object:self userInfo:@{@"beacon": beacon}];
                NSLog(@"Call DidExit");
            }
        }
    });
}

When I am logging the call of the delegate methods I receive the following scheme:

1) DidDetermineState is being called 2) DidEnterRegion is being called 3) but no DidExitRegion is called after that.

also I repeatedly receive this error: "PBRequester failed with Error Error Domain=NSURLErrorDomain Code=-1003 "A server with the specified hostname could not be found." UserInfo=0x166875e0 {NSErrorFailingURLStringKey=https://gsp10-ssl.apple.com/use, NSErrorFailingURLKey=https://gsp10-ssl.apple.com/use, NSLocalizedDescription=A server with the specified hostname could not be found., NSUnderlyingError=0x1656a9b0 "A server with the specified hostname could not be found."}"

this seems very strange.. is there a way I can accomplish to receive the local note every time I select a beacon in the action sheet on my ipad?

Interesting enough, when I leave the Beacon I selected turned on, my iphone keeps throwing up the local notes from time to time without me changing anything in between. And suddenly the DidExitRegion is being called and DidEnterRegion after that again...

thank you!!

sesc360
  • 3,155
  • 10
  • 44
  • 86

1 Answers1

2

It is hard to say exactly what is going on without knowing a bit more what you are doing. Are you continually transmitting the advertisements for the 3 UUIDs, or just transmitting each once then stopping? What do you mean by "Even if I delete the sent old ones and should start with a clean slate"? does this mean stopping ranging on those UUIDs then restarting? Some code snippets might help.

An important thing to know is that iOS continues to track the state of each I beacon it sees even if no monitoring is active. This means that if iBeacons A and B have been detected by iOS before you start monitoring for them, you will get no didEnterRegion notification. Similarly, if after getting such a notification you stop monitoring and restart monitoring, you also won't get a new didEnterRegion notification until iOS thinks the iBeacon disappeared and reappeared.

Are you sure this full transition is happening? Try adding NSLog statements in the didEnterRegion and didExitRegion callbacks to help see the timestamps of when the events are firing.

It is also important to understand that iOS can take a long time to detect state changes if no app with active iBeacon monitors or ranges is in the foreground. I have seen it take up to 4 minutes for each state transition. If you are starting and stopping your transmitter, try waiting at least 8 minutes before restarting and and verify via logs that you then get the two transitions needed for a second notification.

davidgyoung
  • 63,876
  • 14
  • 121
  • 204
  • Hi David, well I am transmitting them continually as far as I think. When I hit the "Stop" button on my ipad app I call the method stopMonitoringAllRegions as you can see posted by me in the code. How can I force iOS to loose track of the beacons so I have a "fresh start" next time I select a beacon again? Interesting enough, the DidEnterRegion and DidDetermineState is only called on the first time I select the beacon on the ipad. The second time I am selecting it, nothing is logged again on the screen.. so the delegate is not called again. – sesc360 Oct 30 '13 at 12:23
  • 1
    Thanks for the edits, those help. Unfortunately, I don't think there is any way to "force" iOS to start fresh. The tracking of iBeacons is handled at the OS level and apps aren't allowed to manipulate this tracking. Calling your stopMonitoringAllRegions method will not affect the state of the OS-level tracking. You simply have to wait enough time (at least 4 min if in the background) before restarting the transmission again. – davidgyoung Oct 30 '13 at 19:33
  • But if we would take a shopping mall as an example where you might have hundreds of beacons which would transmit their signal constantly, and you would have an user strolling around and he would receive the local notes as wanted,and then turns around and goes back... then he would not be able to receive them again? – sesc360 Oct 30 '13 at 20:12
  • And another strange behaviour I had was that the beacon status suddenly changed from insight to unknown even though i hadnt stopped transmitting. is this a normal behaviour? – sesc360 Oct 30 '13 at 20:17
  • 1
    Yes, I have also seen that iOS sometimes calls didExitRegion and then almost immediately calls didEnterRegion. I'm not sure what causes this, but you can filter these out in your code by storing in an NSDate variable the time you last exited the region. When you get a didEnterRegion callback, you simply ignore it if the exit timestamp was within the last few secs. As for your other comment about the Shopping mall use case, please see my answer here: http://stackoverflow.com/questions/19477044/ibeacon-in-the-background-use-cases – davidgyoung Oct 30 '13 at 22:45
  • @davidgyoung Do you have any sample code on git by chance with it alerting in the background? I've been pulling my hair out trying to get it work. I've also submitted a technical support ticket to apple but with no response yet. – random Nov 13 '13 at 22:43
  • I do have some sample code for monitoring in the background, along with a long discussion of how it works. See: http://developer.radiusnetworks.com/2013/11/13/ibeacon-monitoring-in-the-background-and-foreground.html Also, the AppStore just approved my iBeaconLocate app that does alerting in the background. Try it and see if that works for you: https://itunes.apple.com/us/app/ibeacon-locate/id738709014?ls=1&mt=8 – davidgyoung Nov 14 '13 at 00:49