Ranging beacons in background - ios

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

Related

iOS 8 Background Location Update triggered by iBeacon

I am trying to make an app that can be trigged by an iBeacon to wake up (from being killed/suspended/terminated) to record second-by-second GPS information. The GPS recording should then stop when the phone gets out of range of the beacon. I have successfully gotten my app to recognize the didEnterRegion and didExitRegion methods when it comes in and out of range of the iBeacon. In the didEnterRegion method I want to basically say something like [locationManager startUpdatingLocation] so that I can start tracking the user's location. However, when I try to add this line of code, the location updates stop after about 10 seconds.
Later I found an article about background location updates that came with this Github project. I added the BackgroundTaskManager, LocationShareModel, and LocationTracker files to my project. Basically, the idea behind this solution is to continually restart the location manager so it doesn't have the chance for the background task to expire and stop sending updates. However, even with this solution, I only get location updates for a little over 3 minutes.
I have the "Location Updates" and "Use Bluetooth LE accessories" background modes enables. The "Background Fetch" (Background App Refresh) is not enabled, in accordance with this quote from Apple: "In iOS 8 and later, disabling the Background App Refresh setting for the current app or for all apps does not prevent the delivery of location events in the background." My app requests "Always" authorization for location updates.
I cannot figure out how to solve this issue, despite reviewing seemingly endless StackOverflow articles and tutorials. I am testing it on an iPhone 5S running iOS 8.3.0. Any insight would be appreciated. See code excerpts below.
In AppDelegate.m :
- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region {
if ([region isKindOfClass:[CLBeaconRegion class]]) {
UILocalNotification *notification = [[UILocalNotification alloc] init];
notification.alertBody = #"Start recording trip";
notification.soundName = #"Default";
[[UIApplication sharedApplication] presentLocalNotificationNow:notification];
self.recording = YES;
[self startAutoTrip];
}
}
- (void) startAutoTrip {
self.locationTracker = [[LocationTracker alloc]init];
[self.locationTracker startLocationTracking];
}
- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region {
if ([region isKindOfClass:[CLBeaconRegion class]]) {
UILocalNotification *notification = [[UILocalNotification alloc] init];
notification.alertBody = #"Stop recording trip";
notification.soundName = #"Default";
[[UIApplication sharedApplication] presentLocalNotificationNow:notification];
[self stopAutoTrip];
self.recording = NO;
}
}
- (void)stopAutoTrip {
// stop recording the locations
CLSLog(#"Trying to stop location updates");
[self.locationTracker stopLocationTracking:self.managedObjectContext];
CLSLog(#"Stop location updates");
}
In LocationTracker.m (from tutorial cited above, change 60 sec and 10 sec time intervals to 5 sec and 2 sec). Basically these are the startLocationTracking, didUpdateLocations, and stopLocationTracking methods.
- (void)startLocationTracking {
NSLog(#"startLocationTracking");
if ([CLLocationManager locationServicesEnabled] == NO) {
NSLog(#"locationServicesEnabled false");
UIAlertView *servicesDisabledAlert = [[UIAlertView alloc] initWithTitle:#"Location Services Disabled" message:#"You currently have all location services for this device disabled" delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil];
[servicesDisabledAlert show];
} else {
CLAuthorizationStatus authorizationStatus= [CLLocationManager authorizationStatus];
if(authorizationStatus == kCLAuthorizationStatusDenied || authorizationStatus == kCLAuthorizationStatusRestricted){
NSLog(#"authorizationStatus failed");
} else {
NSLog(#"authorizationStatus authorized");
CLLocationManager *locationManager = [LocationTracker sharedLocationManager];
locationManager.delegate = self;
locationManager.desiredAccuracy = kCLLocationAccuracyBest;
locationManager.distanceFilter = 10; //meters
locationManager.activityType = CLActivityTypeAutomotiveNavigation;
locationManager.pausesLocationUpdatesAutomatically = NO;
if(IS_OS_8_OR_LATER) {
[locationManager requestAlwaysAuthorization];
}
[locationManager startUpdatingLocation];
}
}
}
-(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations{
NSLog(#"locationManager didUpdateLocations");
for(int i=0;i<locations.count;i++){
CLLocation * newLocation = [locations objectAtIndex:i];
NSDate *eventDate = newLocation.timestamp;
NSTimeInterval howRecent = [eventDate timeIntervalSinceNow];
if (fabs(howRecent) < 10.0 && newLocation.horizontalAccuracy < 20 && locations.count > 0) {
CLLocationCoordinate2D theLocation = newLocation.coordinate;
CLLocationAccuracy theAccuracy = newLocation.horizontalAccuracy;
self.myLastLocation = theLocation;
self.myLastLocationAccuracy= theAccuracy;
CLLocationCoordinate2D coords[2];
coords[0] = ((CLLocation *)locations.lastObject).coordinate;
coords[1] = newLocation.coordinate;
[self.shareModel.myLocationArray addObject:newLocation];
}
}
//If the timer still valid, return it (Will not run the code below)
if (self.shareModel.timer) {
return;
}
self.shareModel.bgTask = [BackgroundTaskManager sharedBackgroundTaskManager];
[self.shareModel.bgTask beginNewBackgroundTask];
//Restart the locationMaanger after 1 minute (5 sec)
self.shareModel.timer = [NSTimer scheduledTimerWithTimeInterval:5 target:self
selector:#selector(restartLocationUpdates)
userInfo:nil
repeats:NO];
//Will only stop the locationManager after 10 seconds, so that we can get some accurate locations
//The location manager will only operate for 10 seconds to save battery
// 2 sec
if (self.shareModel.delay10Seconds) {
[self.shareModel.delay10Seconds invalidate];
self.shareModel.delay10Seconds = nil;
}
self.shareModel.delay10Seconds = [NSTimer scheduledTimerWithTimeInterval:2 target:self
selector:#selector(stopLocationDelayBy10Seconds)
userInfo:nil
repeats:NO];
}
- (void) restartLocationUpdates
{
NSLog(#"restartLocationUpdates");
if (self.shareModel.timer) {
[self.shareModel.timer invalidate];
self.shareModel.timer = nil;
}
CLLocationManager *locationManager = [LocationTracker sharedLocationManager];
locationManager.delegate = self;
locationManager.desiredAccuracy = kCLLocationAccuracyBest;
locationManager.distanceFilter = 10; //meters
locationManager.activityType = CLActivityTypeAutomotiveNavigation;
locationManager.pausesLocationUpdatesAutomatically = NO;
if(IS_OS_8_OR_LATER) {
[locationManager requestAlwaysAuthorization];
}
[locationManager startUpdatingLocation];
}
- (void)stopLocationTracking:(NSManagedObjectContext *)managedObjectContext {
NSLog(#"stopLocationTracking");
CLSLog(#"stopLocationTracking");
CLSLog(#"set managedObjectContext %#", managedObjectContext);
self.managedObjectContext = managedObjectContext;
if (self.shareModel.timer) {
[self.shareModel.timer invalidate];
self.shareModel.timer = nil;
}
CLLocationManager *locationManager = [LocationTracker sharedLocationManager];
[locationManager stopUpdatingLocation];
[self saveRun];
[self sendRun];
}
Thank you all for your responses. It is possible to wake your app up from being killed/suspended/terminated using iBeacons, contrary to what Øyvind Hauge said. And unfortunately, adding the background location mode to your plist does not enable indefinite location updates, as others suggested; I was only ever able to get 3 minutes of execution using that method.
I actually found the solution to my question in this StackOverflow article. The solution is to add just a few lines of code to your app delegate - you need to start another location manager that is monitoring for significant location updates. Here are the lines of code that I added to my didFinishLaunchingWithOptions method in my AppDelegate.m file after declaring anotherLocationManager as a property...
self.anotherLocationManager = [[CLLocationManager alloc] init];
self.anotherLocationManager.delegate = self;
[self.anotherLocationManager startMonitoringSignificantLocationChanges];
I never do anything else using this location manager, I just leave it perpetually running in the background, and for some reason this enables indefinite location updates from a regular call to [locationManager startUpdatingLocation]. I am no longer having the location updates mysteriously stop after 3 minutes. It seems very strange that this was the solution, but it was pretty simple to implement, and hopefully this will help others who are dealing with the same problem.
If you set the location background mode in your plist, you can range beacons and get GPS location updates indefinitely. The key to getting this to work is starting a background thread.
You can see an example of how to do this in this blog post about extending beacon ranging on the background. While the blog post mentions this is limited to 3 minutes, when you add the location background mode to your plist, that time limit goes away.
Understand that you may not get AppStore approval for using this background mode unless Apple appreciates your justification for doing so.
So in iOS, location updates will work in background indefinitely ONLY if -
1. You have started location updates in foreground AND
2. You have added Background Location in your plist.
In your case, the OS is waking you up in background and as you've said correctly, you only get 10 seconds of execution time before the OS suspends your app. The workaround for this is basically starting a background task, as you have done to get additional 180 seconds of execution time (this number can change based on OS version).
To understand your issue in depth, you need to know that there are only certain events(like geofence/ibeacon/significant location update) which will wake your app in background, let us call them "wakeup" events. Once any of these event occurs, you have a maximum of 180 seconds of background execution time (using background task) after which your app WILL be suspended, unless any of these events is triggered again, after which you need to restart your background task. I'm not sure how your application works exactly, but if you can ensure that you keep getting these "wakeup" events from the OS for the duration for which you need location updates, you can pretty much keep your app awake in background.
Just to add, I've seen a lot of blog posts that claim that keeping a timer and restarting location updates periodically using that timer works, but I have never been able to use it successfully.

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)
}

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)

How do I use CoreLocation to get multiple iBeacons?

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.

didEnterRegion works in foreground but not background or other VCs

If the app is running and the CLLocationManagerDelegate class is the foreground (i.e. visible) then the didEnterRegions triggers and I get both the NSLog as well as the AlertView. However, I get nothing when the app is in the background or, essentially, if the screen is showing anything but the delegate class.
I have set "App registers for location updates" under "Required background modes" in the plist although I'm not sure that's even necessary.
Here's what I think is the relevant code although I may be wrong (and will gladly add more). I should note that everything in viewDidLoad is wrapped in an if which checks if region monitoring is available and enabled.
- (void)viewDidLoad
{
NSLog(#"MapViewController - viewDidLoad");
self.locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters;
self.locationManager.distanceFilter = kCLLocationAccuracyNearestTenMeters;
self.locationManager.delegate = self;
[self.locationManager startMonitoringSignificantLocationChanges];
}
- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region
{
NSLog(#"MapViewController - didEnterRegion");
NSLog(#"MVC - didEnterRegion - region.radius = %f", region.radius);
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"entered region..." message:#"You have Entered the Location." delegate:nil cancelButtonTitle:#"OK" otherButtonTitles: nil];
alert.tag = 2;
[alert show];
}
here is where I get the list of regions being monitored, in AppDelegate.m:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// other code
NSLog(#"LISTING ALL REGIONS MONITORED");
NSArray *regions = [self.locationManager.monitoredRegions allObjects];
if (!regions) {
NSLog(#"no regions found");
} else {
NSLog(#"got %d monitored regions", [regions count]);
for (int i = 0; i < [regions count]; i++) {
CLRegion *region = [regions objectAtIndex:i];
NSLog(#"region %d's identifier = %#", i, region.identifier);
NSLog(#"region: radius: %#", region.radius);
}
}
// other code
}
I call startMonitoringForRegion twice, here's the main place:
- (void)doneButtonTapped {
NSLog(#"doneButtonTapped");
if (self.locationIdentifier) {
if ([CLLocationManager regionMonitoringEnabled] && [CLLocationManager regionMonitoringAvailable]) {
// core data setup
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entityDescription = [NSEntityDescription entityForName:#"LocationReminder" inManagedObjectContext:self.managedObjectContext];
fetchRequest.entity = entityDescription;
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"locationIdentifier == %#", self.locationIdentifier];
fetchRequest.predicate = predicate;
NSError *error;
NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
if (results) {
// get the LocationReminder
LocationReminder *retrievedReminder = [results objectAtIndex:0];
retrievedReminder.audioURI = [[[self.audioPlayers objectAtIndex:self.selectedCell] url] absoluteString];
retrievedReminder.userRecording = nil;
// start monitoring it's region
NSArray *coordinateArray = [retrievedReminder.locationIdentifier componentsSeparatedByString:#", "];
CLLocationCoordinate2D coordinate = {[[coordinateArray objectAtIndex:0] doubleValue], [[coordinateArray objectAtIndex:1] doubleValue]};
CLRegion *newRegion = [[CLRegion alloc] initCircularRegionWithCenter:coordinate radius:250.0 identifier:retrievedReminder.locationIdentifier];
NSLog(#"about to monitor region with radius: %f", newRegion.radius);
[self.locationManager startMonitoringForRegion:newRegion desiredAccuracy:kCLLocationAccuracyBest];
// save the LocationReminder
if (![self.managedObjectContext save:&error]) {
NSLog(#"hmm. no managed object context. must be something space-time going on");
} else {
NSLog(#"saved locationReminder, locationIdentifier = %#", retrievedReminder.locationIdentifier);
}
} else {
NSLog(#"ERROR: no LocationReminder retreived for predicate: %#", predicate);
}
}
// get the mapview controller off of the navigation stack
for (UIViewController *viewController in self.navigationController.viewControllers) {
if ([viewController isKindOfClass:[MapViewController class]]) {
MapViewController *mapVC = (MapViewController *)viewController;
mapVC.audioURI = [[[self.audioPlayers objectAtIndex:self.selectedCell] url] absoluteString];
[self.navigationController popToViewController:mapVC animated:YES];
}
}
}
And because I get the feeling that it might be important, here's the getter for locationManager:
- (CLLocationManager *)locationManager {
NSLog(#"MapViewController - locationManager");
if (_locationManager) {
return _locationManager;
} else {
_locationManager = [[CLLocationManager alloc] init];
return _locationManager;
}
}
UPDATE 1: Via the Apple forums (where I crossposted) someone mentioned that AlertView will only show in the foreground. Still the NSLog doesn't fire either. I'm assuming that should work.
A friend of mine wrote up a nice tutorial on using geofencing that might help clear up some issues you are having.
Get started with geofencing
There are plenty of examples online and here on SO. Start out small and work your way up. Once you start getting your callbacks, you can start expanding things out to your other view controllers.
UPDATE
As explained in the comments the benefits of creating a singleton class to control your location manager and delegate methods. By using a singleton, you prevent the possibility of getting multiple calls to your delegate methods. You can prevent this by careful coding, but using a singleton does this for you. This is also a nice class to handle all the work needing to be done by your delegate methods.
Things you are doing wrong:
Background modes - App registers for location updates. This is not needed. This is need when you want to gather info for significant changes in location etc. So, go to Targets > Your app > Capabilites , and select the desired option under Background modes. This will automatically update the plist for you. For now, disable it.
You are trying to create an alert when the user enters a region. While this while work when app is working, an alert is of no use when your app is in background. Do - Rather trigger a local notification or an api call.
eg. of a notification:
-(void)triggerLocalNotification:(CLRegion *)region{
UILocalNotification *notification = [[UILocalNotification alloc]init];
[notification setAlertBody:[NSString stringWithFormat:#"Welcome to %#", [region identifier]]];
[notification setRepeatInterval:0];
[notification setFireDate:[NSDate dateWithTimeIntervalSinceNow:2]];
[notification setTimeZone:[NSTimeZone defaultTimeZone]];
[[UIApplication sharedApplication]scheduleLocalNotification:notification];
NSLog(#"notification triggered with notification %#", notification);
}
You can post a local notification when you didEnterRegion.
This will show an alert-like popup even if you're in the background.
You can do a simple test:
1) Create a Local notification object inside your applicationDidEnterBackground of your app delegate, with any random message and tell the local notification to fire immediately.
2) Press the home button, when you app minimise, you should see a popup.
i think you need go to your app.plist
and add Required Background modes : add itme App registers for location update
and 1 . if you app is in background , you still see the arrow on top
and 2 , if the app killed , you can still see a hollow arrow on the top , ios will monitor the region for you , but limited to 20 regions

Resources