How do I use CoreLocation to get multiple iBeacons? - ios

I'm trying to use CoreLocation to get multiple iBeacons as specified by my iOS app- the idea is that if they're in range, it'll notify me for each one as it finds them.
The problem is that in the didEnterRegion and didExitRegion methods, I have to provide a CLBeaconRegion object. There's one of these for every iBeacon, and if I was just using one iBeacon, I could just use that one, but since there are several, I need to know how to find each CLBeaconRegion from those methods.
Maybe I'm misunderstanding how this works; if so, please let me know.
- (void)getForUUUIDs:(CDVInvokedUrlCommand *)command
{
//Get an array of UUID's to filter by
self->locationUUIDs = [self getArgsObject:command.arguments];
self->locationManager = [[CLLocationManager alloc] init];
self->locationManager.delegate = self;
scanCallback = command.callbackId;
for(NSInteger i = 0; i < [locationUUIDs count]; i++) {
NSString *identifier = [NSString stringWithFormat:#"BLERegion %d",i];
CLBeaconRegion *thisRegion = [[CLBeaconRegion alloc] initWithProximityUUID:[[locationUUIDs allKeys] objectAtIndex:i] identifier:identifier];
[self->locationManager startMonitoringForRegion:thisRegion];
}
}
- (void)locationManager:(CLLocationManager*)manager didEnterRegion:(CLRegion*)region
{
[self->locationManager startRangingBeaconsInRegion:????];
}
-(void)locationManager:(CLLocationManager*)manager didExitRegion:(CLRegion*)region
{
[self->locationManager stopRangingBeaconsInRegion:????];
}

Ranging on the same region that fired the monitor entry/exit event is extremely simple:
- (void)locationManager:(CLLocationManager*)manager didEnterRegion:(CLRegion*)region
{
[self->locationManager startRangingBeaconsInRegion:(CLBeaconRegion *)region];
}
This will start ranging on the exact same region you used to start monitoring. Note that there is a region parameter passed to the callback. That Region parameter will include the same UUID that you set up before.
One other point: while there is nothing wrong with starting ranging when you enter a region and stopping ranging when you exit a region, there is really no need to do this. Just start ranging the same time you start monitoring. Because ranging won't do anything when the iBeacon isn't visible, the end result will be almost identical. The only difference is you will probably get your first ranging callback one second sooner if you set it up ahead of time. There is no extra drain on battery or system resources.
The added benefit of setting it up ahead of time is that you don't have to do the casting of the CLRegion object -- you have the original object to begin with. And you don't have to implement the monitoring callback methods, so your code is simpler. Like this:
- (void)getForUUUIDs:(CDVInvokedUrlCommand *)command
{
//Get an array of UUID's to filter by
self->locationUUIDs = [self getArgsObject:command.arguments];
self->locationManager = [[CLLocationManager alloc] init];
self->locationManager.delegate = self;
scanCallback = command.callbackId;
for(NSInteger i = 0; i < [locationUUIDs count]; i++) {
NSString *identifier = [NSString stringWithFormat:#"BLERegion %d",i];
CLBeaconRegion *thisRegion = [[CLBeaconRegion alloc] initWithProximityUUID:[[locationUUIDs allKeys] objectAtIndex:i] identifier:identifier];
[self->locationManager startMonitoringForRegion:thisRegion];
[self->locationManager startRangingBeaconsInRegion:thisRegion];
}
}

your region is specified by a uuid
self.beaconRegion = [[CLBeaconRegion alloc] initWithProximityUUID:uuid
identifier:identifier];
All your beacons must share that uuid. So when you range your beacons, you can get them in that method (CLLocationManagerDelegate).
-(void)locationManager:(CLLocationManager*)manager didRangeBeacons:(NSArray*)beacons inRegion:(CLBeaconRegion*)region
{
for (CLBeacon *beacon in beacons) {
NSLog(#"Major : %#", beacon.major);
NSLog(#"Minor : %#", beacon.minor);
}
}
The attributes major and minor are here to differentiate your beacons.

Related

Ranging beacons in background

I want to rang beacons in the background. With background i mean when the phone goes to lock screen. I want the app to continue ranging beacons. The problem i have now is that the code never finds beacons. I have two beacons who is working but the AppDelegate don't find them. When i run the same code in a ViewController, it finds the beacons and displays them. How can i do it?
#interface BDAppDelegate () <AXABeaconManagerDelegate>
#end
#implementation BDAppDelegate {
NSMutableDictionary *beaconRegions;
NSMutableDictionary *detectBeacons;
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
return YES;
}
- (void)applicationWillResignActive:(UIApplication *)application {
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
CLBeaconRegion *beaconRegion = [[CLBeaconRegion alloc] initWithProximityUUID:[[NSUUID alloc] initWithUUIDString:#"MyUUID"] identifier:#"微信"];
[AXABeaconManager sharedManager].beaconDelegate = self;
[[AXABeaconManager sharedManager] requestAlwaysAuthorization];
[[AXABeaconManager sharedManager] startRangingBeaconsInRegion:beaconRegion];
self->beaconRegions = [[NSMutableDictionary alloc] init];
self->detectBeacons = [[NSMutableDictionary alloc] init];
while (detectBeacons.count < 10) {
NSLog(#"Rows in detectBeacons %lu", (unsigned long)beaconRegions.count);
}
self->beaconRegions[beaconRegion] = [NSArray array];
}
- (void)didRangeBeacons:(NSArray *)beacons inRegion:(CLBeaconRegion *)region {
self->beaconRegions[region] = beacons;
NSMutableArray *allBeacons = [NSMutableArray array];
for (NSArray *regionResult in [self->beaconRegions allValues])
{
[allBeacons addObjectsFromArray:regionResult];
}
NSPredicate *pre = [NSPredicate predicateWithFormat:#"accuracy != -1"];
NSArray *rights = [allBeacons filteredArrayUsingPredicate:pre];
NSString * str = #"accuracy";
self->detectBeacons[str] = rights;
}
#end
On iOS, apps are limited to ranging for 5 seconds in the background. This timer is restarted each time the app is put to the background, or when a beacon monitoring event (entered region / exited region) fires. The good news is that you can extend the time allowed to range beacons in the background to 3 minutes after each of these events.
I put together a blog post that shows you how to do it here.
For CLLocationManager there is a method startMonitoringForRegion(CLBeaconRegion *):beaconRegion
which should be added before we start startRangingBeaconsInRegion.
So if your AXABeaconManager class is from CLLocationManager add this:
[[AXABeaconManager sharedManager] startMonitoringForRegion:beaconRegion];
Otherwise:
Create a CLLocationManager object locationManager and initialize it then add start monitoring like below.
[self.locationManager startMonitoringForRegion:beaconRegion];
before you startRangingBeaconsInRegion
Discussion: startMonitoringForRegion
You must call this method once for each region you want to monitor. If an existing region with the same identifier is already being monitored by the app, the old region is replaced by the new one. The regions you add using this method are shared by all location manager objects in your app and stored in the monitoredRegions property.
for more refer here
ranging for beacons is an operation that consumes a lot of battery and iOS won't allow you to do it endlessly in the BG (most of the time. there are cases, where it works)
what you gotta do is call iOS your doing BG work:
UIBackgroundTaskIdentifier token = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
NSLog(#"Ranging for region %# killed", region.identifier);
}];
if(token == UIBackgroundTaskInvalid) {
NSLog(#"cant start background task");
}
THEN do whatever
when done, call endBackgroundTask

Multiple Local Notifications glitch from didEnterRegion:

I'm Currently monitoring several locations that are backed by core data.
In other words, I have set up a for loop that loops through all of the stored entities in core data and creates a monitored region for all of the entities.
The problem here is that the for loop triggers multiple local notifications when entering one of the regions. The number of notifications almost directly corresponds to the number of monitored regions. So I'm fairly confident this may be whats causing the bug, but I'm not 100 percent sure.
I've noticed that this seems to be a common issue with region monitoring, but I haven't been unable to find an example that incorporates a for loop.
How can I stop multiple notifications being triggered when didEnterRegion gets called?
The method below is called in viewDidLoad. The [DataSource sharedInstance].fetchedResultItems is an array that is populated with the fetchedObjects from a fetched request.
-(void)startMonitoringRegions{
if ([self.locationManager respondsToSelector:#selector(requestWhenInUseAuthorization)]) {
[self.locationManager requestWhenInUseAuthorization];
CLAuthorizationStatus authorizationStatus = [CLLocationManager authorizationStatus];
if (authorizationStatus == kCLAuthorizationStatusAuthorizedAlways ||
authorizationStatus == kCLAuthorizationStatusAuthorizedWhenInUse) {
self.locationManager.distanceFilter = 10;
[self.locationManager startUpdatingLocation];
for (POI *items in [DataSource sharedInstance].fetchResultItems){
NSString *poiName = items.name;
NSNumber *poiLatitude = items.yCoordinate;
NSLog(#"value: %#", poiLatitude);
NSNumber *poiLongitude = items.xCoordinate;
NSLog(#"value: %#", poiLongitude);
NSString *identifier = poiName;
CLLocationDegrees latitude = [poiLatitude floatValue];
CLLocationDegrees longitude = [poiLongitude floatValue];
CLLocationCoordinate2D centerCoordinate = CLLocationCoordinate2DMake(latitude, longitude);
self.regionRadius = 10;
self.region = [[CLCircularRegion alloc] initWithCenter:centerCoordinate radius:400 identifier:identifier];
[self.locationManager startMonitoringForRegion:self.region];
NSLog(#"region: %#", self.region);
NSLog(#"monitored regions %#", self.locationManager.monitoredRegions);
}
}
}
}
Here is the didEnterRegion method
- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region{
NSLog(#"entered region!!");
UILocalNotification *localNotification = [[UILocalNotification alloc] init];
if (localNotification) {
localNotification.fireDate = nil;
localNotification.alertBody = [NSString stringWithFormat:#"You are near %#", self.region.identifier];
localNotification.fireDate = [NSDate dateWithTimeIntervalSinceNow:10];
localNotification.timeZone = [NSTimeZone defaultTimeZone];
}
[[UIApplication sharedApplication] scheduleLocalNotification:localNotification];
// [[UIApplication sharedApplication]presentLocalNotificationNow:localNotification];
}
Regions are act as a shared resources. When you enter any region a call will be forwarded to all of the location manager. I think somewhere somehow you are creating multiple location manager objects. That is actually causing the multiple calling of didEnterRegion. The number of time didEnterRegion is called depending upon the number of LocationManager you registered. You should write the code in AppDelegate, in this method
(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//Place your code here
}
Just a troubleshooting tip. You can use Obj-C equivalent of the following to see what regions are currently being monitored by the app. Perhaps reviewing the identifiers will shed some light on the problem.
for region in locationManager.monitoredRegions {
debugPrint(region.identifier)
}
And for a clean start you can delete all regions with this:
for region in locationManager.monitoredRegions {
locationManager.stopMonitoringForRegion(region)
}

How to suspend beacon's notification while in range?

I'm quite a newbie in iOS development and I start working with iBeacon since a couple of weeks. I'm currently developing an app that delivers a coupon to the user while he/she enters a beacon's range (e.g. a store section). This coupon has to be delivered only once in a while, but the user may most likely stay inside the beacon's range even after the delivery is done so I need the app to suspend "listening" to that particular beacon for a fixed amount of time, let's say 30 mins.
This is my implementation of the locationManager:didRangeBeacons:inRegion::
- (void)locationManager:(CLLocationManager *)manager
didRangeBeacons:(NSArray *)beacons
inRegion:(CLBeaconRegion *)region {
if (foundBeacons.count == 0) {
for (CLBeacon *filterBeacon in beacons) {
// If a beacon is located near the device and its major and minor values are equal to some constants
if (((filterBeacon.proximity == CLProximityImmediate) || (filterBeacon.proximity == CLProximityNear)) && ([filterBeacon.major isEqualToNumber:[NSNumber numberWithInt:MAJOR]]) && ([filterBeacon.minor isEqualToNumber:[NSNumber numberWithInt:MINOR]]))
// Registers the beacon to the list of recognized beacons
[foundBeacons addObject:filterBeacon];
}
}
// Did some beacon get found?
if (foundBeacons.count > 0) {
// Takes first beacon of the list
beacon = [foundBeacons firstObject];
if (([beacon.major isEqualToNumber:[NSNumber numberWithInt:MAJOR]]) && ([beacon.minor isEqualToNumber:[NSNumber numberWithInt:MINOR]])) {
// Plays beep sound
AudioServicesPlaySystemSound(soundFileObject);
if (self.delegate) {
// Performs actions related to the beacon (i.e. delivers a coupon)
[self.delegate didFoundBeacon:self];
}
self.locationManager = nil;
}
[foundBeacons removeObjectAtIndex:0];
beacon = nil;
}
}
How can I add some timer or something related to make the app ignore the beacon for a while?
A common technique is to keep a data structure that tells you when you last took action on a beacon, and then avoid taking action again if enough time has not passed since you last did so.
The following example shows how you can add a 10 minute (600 second) filter on repeating beacon events.
// Declare these in your class
#define MINIMUM_ACTION_INTERVAL_SECONDS 600
NSMutableDictionary *_lastBeaconActionTimes;
...
// Initialize in your class constructor or initialize method
_lastBeaconActionTimes = [[NSMutableDictionary alloc] init];
...
// Add the following before you take action on the beacon
NSDate *now = [[NSDate alloc] init];
NSString *key = [NSString stringWithFormat:#"%# %# %#", [beacon.proximityUUID UUIDString], beacon.major, beacon.minor];
NSDate *lastBeaconActionTime = [_lastBeaconActionTimes objectForKey:key];
if (lastBeaconActionTime == Nil || [now timeIntervalSinceDate:lastBeaconActionTime] > MINIMUM_ACTION_INTERVAL_SECONDS) {
[_lastBeaconActionTimes setObject:now forKey:key];
// Add your code to take action here
}

iBeacon major and minor value inside didEnterRegion

I'm trying to access the major and minor values for the closest beacon within the didEnterRegion delegate. However, when printing the values to the console they return null
- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region {
if ([region isKindOfClass:[CLBeaconRegion class]]) {
CLBeaconRegion *beaconRegion = (CLBeaconRegion *)region;
int major = [beaconRegion.major intValue];
int minor = [beaconRegion.minor intValue];
NSLog(#" Major %# Minor %#", beaconRegion.major, beaconRegion.minor);
}
}
The region monitoring callback you have implemented will not tell you the individual identifiers of the beacons you detect. If you want to get the identifiers for individual beacons detected, you have to use the beacon ranging APIs as #Larme says in his comment. The callback for ranging includes a second parameter that is an array of all beacons seen.
You have to differentiate between Monitoring and Ranging iBeacons. Only successfully ranging iBeacons provides you with the Major/Minor IDs.
Looks like you are not initializing the BeaconRegion with minor and major values
While initializing the beacon region you need to use
initWithProximityUUID:major:minor:identifier:
instead of
initWithProximityUUID:identifier:
If you do not want to initialize minor and major values into regions, then you may want to call didRangeBeacons method as mentioned in the comments.
you're trying to get region's major and minor values. but you say that want to get the beacon's values.
it depends on which beacon brand you're using but there must be a method that returns the beacons array the device has found. generally the first object of the array is the closest one.
in that method you can get the beacon's values.
an example code:
- (void)beaconManager:(ESTBeaconManager *)manager didRangeBeacons:(NSArray *)beacons inRegion:(ESTBeaconRegion *)region
{
if([beacons count] > 0)
{
// beacon array is sorted based on distance
// closest beacon is the first one
ESTBeacon* closestBeacon = [beacons objectAtIndex:0];
NSString* theMajor = [NSString stringWithFormat:#"%#",closestBeacon.major];
}
}
Setup locationManager (remember to configure plist for iOS 8 to add in these 2 values NSLocationAlwaysUsageDescription, NSLocationWhenInUseUsageDescription).
#property (strong, nonatomic) CLLocationManager *locationManager;
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
[[UIApplication sharedApplication] cancelAllLocalNotifications];
// Needed for iOS 8
if([self.locationManager respondsToSelector:#selector(requestAlwaysAuthorization)]) {
[self.locationManager requestAlwaysAuthorization];
}
Then call startRangingItem:
- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region{
if ([region isKindOfClass:[CLBeaconRegion class]]) {
[self startRangingItem];
}
}
- (void)startRangingItem {
CLBeaconRegion *beaconRegion = [self beaconRegionWithItem];
[self.locationManager startRangingBeaconsInRegion:beaconRegion];
}
- (CLBeaconRegion *)beaconRegionWithItem{
NSUUID *iPadTransmitterUUID = [[NSUUID alloc] initWithUUIDString:#"AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEFFFFF1"];
CLBeaconRegion *beaconRegion = [[CLBeaconRegion alloc] initWithProximityUUID:iPadTransmitterUUID identifier:#"Transmitter1"];
return beaconRegion;
}
Then in didRangeBeacons:***
- (void)locationManager:(CLLocationManager *)manager didRangeBeacons:(NSArray *)beacons inRegion:(CLBeaconRegion *)region{
CLBeacon *testBeacon = [beacons objectAtIndex:0];
NSLog(#"Inside didRangeBeacons Major %# Minor %#", testBeacon.major, testBeacon.minor);
}
***Please note that this didRangeBeacons method will only run in background for 5 seconds once the user enter the region. It will stop ranging after 5 seconds. If you want to continue ranging, the user needs to launch the app. (forcing the didRangeBeacons to run in background is possible, but it might get rejected by apple)

Detecting iBeacons with different UUIDs

I'm trying to use UUIDs as identifiers for specific beacons (using phones in this case). I understand that major and minor are used to do this, but I'd rather use the UUID, or the identifier string.
With that being said, is there anyway to scan for beacons regardless of UUIDs with the CLBeacon API?
On Android you can scan for all UUIDs. On iOS you cannot. See:
http://developer.radiusnetworks.com/2013/10/21/corebluetooth-doesnt-let-you-see-ibeacons.html
On iOS, CoreLocation limits you to monitoring for at most 20 different UUIDs. Ranging does not have a limit of 20, but you still must know the UUIDs ahead of time.
As far as I know, there is no restriction in monitoring iBeacon regions with different UUID. You can do something like:
NSArray *uuids = [NSArray arrayWithObjects:#"####-####-###1", #"####-####-###2", nil];
for (NSString *uuidString in uuids) {
CLBeaconRegion *region = [[CLBeaconRegion alloc] initWithProximityUUID:[[NSUUID alloc] initWithUUIDString:uuidString] identifier:identifier];
region.notifyOnEntry = entry;
region.notifyOnExit = exit;
region.notifyEntryStateOnDisplay = YES;
[_locationManager startMonitoringForRegion:region];
}
You just need to make sure you check for the UUID in you locationManager's delegate methods. You can use something like this:
- (void)locationManager:(CLLocationManager *)manager didDetermineState:(CLRegionState)state forRegion:(CLRegion *)region {
if ([region isKindOfClass:[CLBeaconRegion class]]) {
CLBeaconRegion *beaconRegion = (CLBeaconRegion *)region;
NSLog(#"ProximityUUID:%#", beaconRegion.proximityUUID);
NSLog(#"ProximityMajor:%#", beaconRegion.major);
NSLog(#"ProximityMinorD:%#", beaconRegion.minor);
}
}
You should see that the delegate method is called with different UUIDs.
Hope this helps.

Resources