What I need:
A predictable, solid and reliable way of launching iBeacon delegate methods such as didDetermineState, didRangeBeacons, didEnterRegion or didExitRegion when the app is dead and the device is plugged in and nearby.
The Current Situation
I am making an app for parents to use for their kids to help them shut down their phones during important times. The app is in Objective-C and it needs to maintain a persistent connection to a bluetooth device even after the life of the application.
I have been trying for a long time to get this to work and I have had help from a lot of S.O. posters and currently I know that I must use iBeacon in my device to launch from terminated (that's the only reason I use it, I would gladly dump it if there was another way to launch an app from terminated). To clarify, I need 2 things here in the same device (which I have already built) iBeacon and a solid BT connection. I need this device connection pairing because this is the only way to send/receive commands from the BT device. What I have discovered is that the didRange or didEnter delegate methods that fire in the background are unreliable at best. They don't always fire right away and they only fire a few times and the whole thing dies (which I now know this 10 second window is expected behaviour from a terminated app). I have even had full entire days where I plug/unplug it constantly looking for any sign that the app has come back to life and nothing happens...
When the app is open, things work fine, however when the app is nearby to my beacon/bluetooth I want it to launch a sort of makeshift lock screen inside the app. I am already doing this part fairly well when the app is in the foreground. If a kid tries to close the app or background it I want to respond by having my BT device launch into the background once it's terminated (I know the UI won't come up and that's fine I just need a series of functions to fire). It will then connect to bluetooth and receive some commands from the device. Sounds simple enough eh? Here's were things get messy.
Some context: I have all background modes added in info.plist for bluetooth and beacon and everything works fine when the app is in foreground...
If the iBeacon is detected in range, I then want to use that 10 second window to connect via BT pairing to my box and send through a command. So far, this is wonky... The iBeacon ranging functions do not fire when the app is terminated they only fire on the strangest of use cases. I cannot seem to predict when they are going to fire.
My Code
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
I have built a similar system for iOS that uses iBeacon transmissions to wake up in the background and then connect to bluetooth LE to exchange data. Rest assured that this is all possible, it's just tricky to get working and trickier to debug.
A few tips on doing this with Bluetooth LE connecting:
Beacon ranging functions will not fire when the app is killed unless you also monitor for the beacons and get a didEnter or didExit transition, which will re-launch the app into the background for 10 secs as you describe. Again, this will only happen if you transition from in region to out of region or vice versa. This is tricky to test, because you may not realize CoreLocation thinks you are "in region" when you kill the app, but you won't get a wakeup event for detecting the beacon.
In order to get bluetooth events in the background, you need to make sure your Info.plist declares this:
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
</array>
If that is not present, you absolutely will not get callbacks to didDiscoverPeripheral in the background.
You will need to start scanning for bluetooth when your app starts up, and connect when you get a callback to func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber)
Save off a copy of the peripheral instance from above because you only get one callback in the background for discovery from each unique bluetooth device. If your connection fails, you can retry with the same peripheral object instance.
In order to debug re-launching from a killed state, I add lots of NSLog statements (I add the ability to turn them on and off in code) and then look for these in XCode's Windows -> Devices -> My iPhone panel, where you can expand the little arrow at the bottom of the screen to show the logs for all apps on the device. You absolutely will see logs here for your app if it is relaunched from a killed state.
Related
I'm starting to work with Kontakt.io beacons on iOS, but even when I've followed the instructions on https://developer.kontakt.io/ios-sdk/quickstart/detecting-beacons/ and the first steps described on https://developer.kontakt.io/ios-sdk/quickstart/installation/, seems that I can only make it works once.
Here's my ViewController's code:
#import "ViewController.h"
#import <KontaktSDK/KontaktSDK.h>
#interface ViewController () <KTKBeaconManagerDelegate>
#property KTKBeaconManager *beaconManager;
#property KTKBeaconRegion *region1;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.beaconManager = [[KTKBeaconManager alloc] initWithDelegate:self];
NSUUID *myProximityUUID = [[NSUUID alloc] initWithUUIDString:#"xxxxxxxxx...xxxx"];
_region1 = [[KTKBeaconRegion alloc] initWithProximityUUID:myProximityUUID identifier:#"Beacon_1"];
switch ([KTKBeaconManager locationAuthorizationStatus]) {
case kCLAuthorizationStatusNotDetermined:
[self.beaconManager requestLocationAlwaysAuthorization];
break;
case kCLAuthorizationStatusDenied:
case kCLAuthorizationStatusRestricted:
// No access to Location Services
break;
case kCLAuthorizationStatusAuthorizedWhenInUse:
// For most iBeacon-based app this type of
// permission is not adequate
break;
case kCLAuthorizationStatusAuthorizedAlways:
// We will use this later
if ([KTKBeaconManager isMonitoringAvailable]) {
[self.beaconManager startMonitoringForRegion:_region1];
}
break;
}
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)beaconManager:(KTKBeaconManager *)manager didChangeLocationAuthorizationStatus:(CLAuthorizationStatus)status {
if (status == kCLAuthorizationStatusAuthorizedAlways) {
_TheLabel.text = #"YEAH";
if ([KTKBeaconManager isMonitoringAvailable]) {
_TheLabel.text = #"YEAH!!!";
[self.beaconManager startMonitoringForRegion:_region1];
}
// When status changes to kCLAuthorizationStatusAuthorizedAlways
// e.g. after calling [self.beaconManager requestLocationAlwaysAuthorization]
// we can start region monitoring from here
}
}
//
- (void)beaconManager:(KTKBeaconManager *)manager didStartMonitoringForRegion:(__kindof KTKBeaconRegion *)region {
// Do something when monitoring for a particular
// region is successfully initiated
_MonitoringStatus.text = #"Success";
[manager startRangingBeaconsInRegion:region];
}
- (void)beaconManager:(KTKBeaconManager *)manager monitoringDidFailForRegion:(__kindof KTKBeaconRegion *)region withError:(NSError *)error {
// Handle monitoring failing to start for your region
_MonitoringStatus.text = #"FAIL!";
}
- (void)beaconManager:(KTKBeaconManager *)manager didEnterRegion:(__kindof KTKBeaconRegion *)region {
// Decide what to do when a user enters a range of your region; usually used
// for triggering a local notification and/or starting a beacon ranging
[manager startRangingBeaconsInRegion:region];
_OnRegionStatus.text = #"We're in!";
}
- (void)beaconManager:(KTKBeaconManager *)manager didExitRegion:(__kindof KTKBeaconRegion *)region {
// Decide what to do when a user exits a range of your region; usually used
// for triggering a local notification and stoping a beacon ranging
[manager stopRangingBeaconsInRegion:region];
_OnRegionStatus.text = #"We're out";
}
- (void)beaconManager:(KTKBeaconManager *)manager didRangeBeacons:(NSArray<CLBeacon *> *)beacons inRegion:(__kindof KTKBeaconRegion *)region {
for(CLBeacon *beacon in beacons){
_TheLabel.text = [NSString stringWithFormat:#"WOW! %ld", (long)[beacon proximity]];
}
}
#end
Playing with breakpoints, seems that the didChangeLocationAuthorizationStatus is always being launched (I can see the "YEAH!!!" message on screen every time) but the didStartMonitoringForRegion is not launched unless I uninstall and reinstall (killing app is not working). It does range beacons fine that first time installed, by the way. As you can see, I've tried just ranging beacons without checking the onEnterRegion, but it didn't work.
EDIT: Updated the viewDidLoad with:
case kCLAuthorizationStatusAuthorizedAlways:
// We will use this later
if ([KTKBeaconManager isMonitoringAvailable]) {
if([[self.beaconManager monitoredRegions] count] == 0) [self.beaconManager startMonitoringForRegion:_region1];
else for(KTKBeaconRegion *reg in [self.beaconManager monitoredRegions]){
[self.beaconManager startRangingBeaconsInRegion:reg];
}
}
break;
And this time is working as expected. But I'm a bit confused about this behaviour. The app keeps the region monitorized, even when it was forcefully closed? Thanks in advance!
As far as I know, Kontakt.io's SDK is based on Apple's Core Location, so the following also applies:
In iOS, regions associated with your app are tracked at all times,
including when the app isn’t running. - source
I have noticed that whenever I start again the app and look at the following method I get that the beaconRegion returned is not nill. How does this work precisely?
beaconRegion = [self.locationManager.monitoredRegions member:beaconRegion];
// for more context on how I use this please look at the code below
In other words, how does iOS handle the allocation of CLLocationManager? Does it deserialise it every time the app is woken up and in this way retrieve the region information?
This is the output of the Xcode debugger console when running the code below:
2015-11-11 09:44:13.718 RegionMonitoringTest[239:15121] AppDelegate: creating new location manager object
2015-11-11 09:44:13.722 RegionMonitoringTest[239:15121] BeaconMonitoring class: in startRangingForBeacons, startupdatinglocation, range for my beacon
2015-11-11 09:44:13.724 RegionMonitoringTest[239:15121] Region already in list
2015-11-11 09:44:13.732 RegionMonitoringTest[239:15121] AppDelegate: Application did became active.
2015-11-11 09:44:13.762 RegionMonitoringTest[239:15121] BeaconMonitoring -> LocationManager: didFailWithError | Error: Error Domain=kCLErrorDomain Code=0 "(null)"
2015-11-11 09:44:13.762 RegionMonitoringTest[239:15121] Requesting to start ranging for beacons again.
2015-11-11 09:44:13.762 RegionMonitoringTest[239:15121] BeaconMonitoring class: in startRangingForBeacons, startupdatinglocation, range for my beacon
2015-11-11 09:44:13.767 RegionMonitoringTest[239:15121] Region already in list
Below I paste the source code that I am using to test this, it may be helpful (it is based on the AirLocate example provided by Apple):
#import "AppDelegate.h"
#define BEACON_REGION #"01020102-0102-0102-0102-010201020102"
#interface AppDelegate ()
#property (strong, nonatomic) CLLocationManager *locationManager;
#property (strong, nonatomic) NSMutableArray * monitoredRegions;
#end
#implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
if ([launchOptions objectForKey:UIApplicationLaunchOptionsLocationKey]) {
NSLog(#"AppDelegate: being woken up after entering region");
}
else{
NSLog(#"AppDelegate: creating new location manager object");
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.pausesLocationUpdatesAutomatically = false;
self.locationManager.allowsBackgroundLocationUpdates = true;
self.locationManager.delegate = self;
if([self.locationManager respondsToSelector:#selector(requestAlwaysAuthorization)]) {
[self.locationManager requestAlwaysAuthorization];
}
self.monitoredRegions = [[NSMutableArray alloc] initWithCapacity:10];
[self startRangingForBeacons];
}
return YES;
}
- (void)applicationWillResignActive:(UIApplication *)application {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
NSLog(#"AppDelegate: will resign active");
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
NSLog(#"AppDelegate: did enter background");
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
//[self.bluetoothDataSync stopScanning];
NSLog(#"AppDelegate: did enter foreground");
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
NSLog(#"AppDelegate: Application did became active.");
}
- (void)applicationWillTerminate:(UIApplication *)application {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
//[[UIApplication sharedApplication] cancelAllLocalNotifications];
NSLog(#"AppDelegate: App will terminate");
}
//////////////////////////////////////////////////
- (void) startRangingForBeacons{
NSLog(#"BeaconMonitoring class: in startRangingForBeacons, startupdatinglocation, range for my beacon");
[self startMonitoringForRegion:[[NSUUID alloc] initWithUUIDString:BEACON_REGION] :#"my-beaconregion"];
}
- (void)locationManager:(CLLocationManager *)manager monitoringDidFailForRegion:(CLRegion *)region withError:(NSError *)error {
NSString * message = [NSString stringWithFormat:#"BeaconMonitoring -> LocationManager: monitoringDidFailForRegion | Error: %#, Region identifier: %#", error, region.identifier];
NSLog(#"%#", message);
NSLog(#"Requesting to start ranging for beacons again.");
[self startRangingForBeacons];
}
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error {
NSString * message = [NSString stringWithFormat:#"BeaconMonitoring -> LocationManager: didFailWithError | Error: %#", error];
NSLog(#"%#", message);
NSLog(#"Requesting to start ranging for beacons again.");
[self startRangingForBeacons];
}
- (void) startMonitoringForRegion:(NSUUID*)beaconUUID :(NSString*)regionIdentifier{
CLBeaconRegion *beaconRegion = nil;
beaconRegion = [self.locationManager.monitoredRegions member:beaconRegion];
if(beaconRegion)
{
NSLog(#"Region already in list");
}
else{
beaconRegion = [[CLBeaconRegion alloc] initWithProximityUUID:beaconUUID identifier:regionIdentifier];
beaconRegion.notifyEntryStateOnDisplay = YES;
beaconRegion.notifyOnEntry = YES;
beaconRegion.notifyOnExit = YES;
[self.locationManager startMonitoringForRegion:beaconRegion];
}
[self.locationManager startRangingBeaconsInRegion:beaconRegion];
[self.locationManager startUpdatingLocation];
[self.monitoredRegions addObject:beaconRegion];
}
- (void) stopRangingForbeacons{
NSLog(#"BeaconMonitoring: stopRangingForbeacons - Stops updating location");
[self.locationManager stopUpdatingLocation];
for (int i=0; i < [self.monitoredRegions count]; i++) {
NSObject * object = [self.monitoredRegions objectAtIndex:i];
if ([object isKindOfClass:[CLBeaconRegion class]]) {
CLBeaconRegion * region = (CLBeaconRegion*)object;
[self.locationManager stopMonitoringForRegion:region];
[self.locationManager stopRangingBeaconsInRegion:region];
}
else{
NSLog(#"BeaconMonitoring: unrecongized object in beacon region list");
}
}
}
- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status
{
if (![CLLocationManager locationServicesEnabled]) {
NSLog(#"Couldn't turn on ranging: Location services are not enabled.");
}
if ([CLLocationManager authorizationStatus] != kCLAuthorizationStatusAuthorized) {
NSLog(#"Couldn't turn on monitoring: Location services not authorised.");
}
}
#pragma CLLocationManagerDelegate
-(void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region {
NSLog(#"BeaconMonitoring: did enter region, will now start ranging beacons in this region");
[manager startRangingBeaconsInRegion:(CLBeaconRegion*)region];
[self.locationManager startUpdatingLocation];
}
-(void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region {
NSLog(#"BeaconMonitoring: did exit region, will now: stop ranging beacons in this region; stop updating locations; stop scanning for BLE");
[manager stopRangingBeaconsInRegion:(CLBeaconRegion*)region];
[self.locationManager stopUpdatingLocation];
NSDictionary * notificationData = #{ #"value" : #"exitedRegion"};
[[NSNotificationCenter defaultCenter] postNotificationName:#"dataUpdate" object:nil userInfo:notificationData];
}
- (void) locationManager:(CLLocationManager *)manager didDetermineState:(CLRegionState)state forRegion:(CLRegion *)region
{
NSString * message = #"CLRegionState: ";
switch (state) {
case CLRegionStateInside:
message = #"CLRegionState: state inside";
break;
case CLRegionStateOutside:
message = #"CLRegionState: state outside";
break;
case CLRegionStateUnknown:
message = #"CLRegionState: state unknown";
break;
default:
message = #"CLRegionState: default case";
break;
}
NSLog(#"%#", message);
}
-(void)locationManager:(CLLocationManager *)manager didRangeBeacons:(NSArray *)beacons inRegion:(CLBeaconRegion *)region {
bool rangedBeacons = false;
if ([beacons count]>0) {
for (int i=0; i<[beacons count]; i++) {
CLBeacon *beacon = [beacons objectAtIndex:i];
if((beacon.major.intValue == 4) && (beacon.major.intValue == 8)){
rangedBeacons = true;
}
}
}
if (rangedBeacons) {
NSLog(#"Region identifier: %#", region.identifier);
NSString * message = [NSString stringWithFormat:#"BeaconMonitoring: ranged a total of %lu, hence request scan start", (unsigned long)[beacons count]];
NSLog(#"%#", message);
}
}
Since Location Manager's monitoring works even when the app is not running, e.g., was terminated due to memory pressure or killed by the user at the app switcher, iOS needs to keep the list of regions apps monitor for somewhere outside the app, and you can think of monitoredRegions as the reflection of that list—rather than an instance property that vanishes when the object gets deallocated.
Note for example that if you instantiate more than one location manager, they all share the same monitoring list—a direct outcome of the list being stored per-app somewhere on the OS level. If you start/stop monitoring for a region in one location manager, it'll affect all the others.
All of this applies to CLBeaconRegion, but also to "regular" CLCircularRegion—because it's a feature of monitoring, and not something beacon-specific.
My customer asked if it is possible that when a customer walks by his store he receives an email with todays special prices, even if the app is not running.
My question is: Is it allowed by iOS to call a restservice if the app is wakened by the iBeacon event?
I tried to play a system sound to simulate the restservice call and this is not working. Only when the app is in foreground.
To give you an idea how I designed my Beaconhandler so far, here is my code. Perhaps someone has an idea to improve it:
#import "BeaconHandler.h"
#interface BeaconHandler ()
#property (strong, nonatomic) CLLocationManager *locationManager;
#property CLProximity lastProximity;
#end
#implementation BeaconHandler
-(void) startMonitoring{
if(![self monitoringIsAllowed]){
return;
}
[self initLocationManager];
[self startMonitoringBeacons:[self beaconIDsToMonitor]];
}
-(BOOL) monitoringIsAllowed{
//TODO configuration
return YES;
}
-(NSDictionary*) beaconIDsToMonitor{
//TODO: load beacons from server
return #{#"region1":[[NSUUID alloc] initWithUUIDString:#"B9407F30-F5F8-466E-AFF9-25556B57FE6D"],
#"region2":[[NSUUID alloc] initWithUUIDString:#"B9407F30-F5F8-466E-AFF9-25556B57FE6A"]
};
}
-(void) initLocationManager{
self.locationManager = [[CLLocationManager alloc] init];
if([self.locationManager respondsToSelector:#selector(requestAlwaysAuthorization)]) {
[self.locationManager requestAlwaysAuthorization];
}
self.locationManager.delegate = self;
self.locationManager.pausesLocationUpdatesAutomatically = NO;
}
-(void) startMonitoringBeacons:(NSDictionary*)beacons{
for (NSString* beaconIdentifier in beacons.allKeys) {
NSUUID *beaconUUID = beacons[beaconIdentifier];
CLBeaconRegion *beaconRegion = [[CLBeaconRegion alloc] initWithProximityUUID:beaconUUID identifier:beaconIdentifier];
beaconRegion.notifyEntryStateOnDisplay = YES;
[self.locationManager startMonitoringForRegion:beaconRegion];
[self.locationManager startRangingBeaconsInRegion:beaconRegion];
}
[self.locationManager startUpdatingLocation];
}
//TODO replace by backend call
-(void)sendLocalNotificationWithMessage:(NSString*)message{
UILocalNotification *notification = [[UILocalNotification alloc] init];
notification.alertBody =message;
[[UIApplication sharedApplication] presentLocalNotificationNow:notification];
}
#pragma CLLocationDelegate
-(void)locationManager:(CLLocationManager *)manager didRangeBeacons:(NSArray *)beacons inRegion:(CLBeaconRegion *)region {
for (CLBeacon *beacon in beacons) {
if(beacon.proximity == self.lastProximity ||
beacon.proximity == CLProximityUnknown) {
return;
}
self.lastProximity = beacon.proximity;
[self sendLocalNotificationWithMessage:[NSString stringWithFormat:#"You are inside region %#", region.identifier]];
}
}
Yes, this is possible. I can confirm I have done this successfully on iOS. A couple of tips to get it running in the background:
Get this working in the foreground first.
You only have 5 seconds of background running time after entering/exiting a region, so make sure your web service returns quickly.
Add NSLog statements to your callbacks to figure out what is and is not completing in the background.
If the above does not help, post your code in the callback.
What I am trying to achieve is described in the following steps
1.User opens the app, navigates blah blah blah
2.User presses the home button, the app shuts itself
3.A background task for detecting CLbeacons starts. On every detection, A UILocalNotification is fired immediately, so as to make the user aware that a beacon has been discovered.
Now what happens with me
I open the app, navigate then press the home button to shut the app
The phone slowly darkens itself, i.e it goes to the slide to unlock state. Here no notification gets fired; Hell, the CLLocationManger's method -(void)locationManager:(CLLocationManager *)manager didRangeBeacons:(NSArray *)beacons inRegion:(CLBeaconRegion *)region doesn't get called at all.
The phone goes to sleep. I slide open the phone to awake it; Immediately multiple `UIlocalNotifications enter.
Can anyone explain to me why is this happening?
//
// AppDelegate.m
// Beacon Proximity
//
// Created by Debanjan Chakraborty on 7/15/14.
// Copyright (c) 2014 Debanjan Chakraborty. All rights reserved.
//
#import "AppDelegate.h"
#import "CustomerRegViewController.h"
#import "IntialViewController.h"
#import "BeaconProduct.h"
#import "DBManager.h"
#import "APLDefaults.h"
#import "ScanViewController.h"
typedef NS_ENUM(NSUInteger, BeaconType)
{
Check,
Bundle
};
#interface AppDelegate ()<CLLocationManagerDelegate>
#property CLLocationManager *locationAppManager;
#property (nonatomic) NSInteger localNotificationCount;
//#property (nonatomic,strong) UILocalNotification *localNotification;
//#property (nonatomic) NSTimer *timerForNotifier;
#end
#implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
if ([[NSUserDefaults standardUserDefaults] boolForKey:#"HasLaunchedOnce"])
{
// app already launched
}
else
{
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:#"HasLaunchedOnce"];
[[NSUserDefaults standardUserDefaults] synchronize];
[self entryDatabase];
}
[[UIApplication sharedApplication] cancelAllLocalNotifications];
[[UIApplication sharedApplication] registerForRemoteNotificationTypes:
(UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound | UIRemoteNotificationTypeAlert)];
UILocalNotification *notification = [launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey];
if (notification)
{
NSLog(#" aache ?");
// self.localNotificationCount = 0;
UIAlertView *aw = [[UIAlertView alloc] initWithTitle:#"Case 1" message: notification.alertBody delegate:self cancelButtonTitle:#"Ok" otherButtonTitles:nil];
[aw show];
}
application.applicationIconBadgeNumber = 0;
return YES;
}
- (void)application:(UIApplication *)app didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
NSString *token = [[deviceToken description] stringByTrimmingCharactersInSet: [NSCharacterSet characterSetWithCharactersInString:#"<>"]];
token = [token stringByReplacingOccurrencesOfString:#" " withString:#""];
NSLog(#"content---%#", token);
}
- (void)application:(UIApplication *)app didFailToRegisterForRemoteNotificationsWithError:(NSError *)err {
NSLog(#"Error %#",err.localizedDescription);
}
- (void)applicationWillResignActive:(UIApplication *)application
{
if(![[NSUserDefaults standardUserDefaults] boolForKey:#"HasLaunchedOnce"])
{
NSLog(#" Will REsign and initialise");
}
else
{
NSLog(#" Will REsign?");
}
}
-(void)applicationWillTerminate:(UIApplication *)application
{
NSLog(#" terminated");
}
- (void)applicationDidEnterBackground:(UIApplication *)application
{
[[UIApplication sharedApplication] cancelAllLocalNotifications];
[self initialise];
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
- (void)applicationWillEnterForeground:(UIApplication *)application
{
application.applicationIconBadgeNumber=0;
for (NSUUID *uuid in [APLDefaults sharedDefaults].supportedProximityUUIDs)
{
CLBeaconRegion *region = [[CLBeaconRegion alloc] initWithProximityUUID:uuid identifier:[uuid UUIDString]];
[self.locationAppManager stopMonitoringForRegion:region];
}
self.locationAppManager = nil;
}
-(void)insert:(NSDate *)fire WithBeaconType:(BeaconType )beaconType ForBeacon:(CLBeacon *)beacon
{
self.localNotificationCount++;
UILocalNotification *localNotification = [UILocalNotification new];
localNotification.soundName = UILocalNotificationDefaultSoundName;
localNotification.applicationIconBadgeNumber =self.localNotificationCount;
localNotification.fireDate = [NSDate date];
localNotification.alertAction = #"Show me";
if(beaconType == Check)
{
localNotification.alertBody = [NSString stringWithFormat:#"Check out new offers"];
localNotification.alertAction = NSLocalizedString(#"Read Offer", nil);
}
else
{
localNotification.alertBody = [NSString stringWithFormat:#"You've reached offers. Grab them before they expire soon"];
localNotification.alertAction = NSLocalizedString(#"Grab Offer", nil);
}
[[UIApplication sharedApplication] scheduleLocalNotification:localNotification];
NSLog(#" should Notify minor is %d and major is %d",[beacon.minor integerValue],[beacon.major integerValue]);
[[DBManager getSharedInstance] updateNotifiedTimeForBeacon:beacon];
}
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification
{
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
[[UIApplication sharedApplication] setApplicationIconBadgeNumber:0];
self.localNotificationCount = 0;
[[UIApplication sharedApplication] cancelAllLocalNotifications];
}
-(UIViewController *)omegaScreen
{
UIStoryboard *strybrd = [UIStoryboard storyboardWithName:#"Storyboard" bundle:nil];
NSString *viewId=[[NSUserDefaults standardUserDefaults]objectForKey:#"custId"]?#"initial":#"custReg";
UIViewController *controller=[strybrd instantiateViewControllerWithIdentifier:viewId];
if([[NSUserDefaults standardUserDefaults] objectForKey:#"custId"])
[self initialise];
return [viewId isEqualToString:#"initial"]?(IntialViewController *)controller:(CustomerRegViewController *)controller;
}
-(void)initialise
{
self.locationAppManager = [[CLLocationManager alloc] init];
self.locationAppManager.delegate = self;
for (NSUUID *uuid in [APLDefaults sharedDefaults].supportedProximityUUIDs)
{
NSLog(#" uuid is %#",uuid.UUIDString);
CLBeaconRegion *region = [[CLBeaconRegion alloc] initWithProximityUUID:uuid identifier:[uuid UUIDString]];
region.notifyEntryStateOnDisplay=YES;
[self.locationAppManager startMonitoringForRegion:region];
}
self.localNotificationCount = -1;
}
- (void) locationManager:(CLLocationManager *)manager didStartMonitoringForRegion:(CLRegion *)region
{
NSLog(#" it did");
if([region isKindOfClass:[CLBeaconRegion class]])
{
[self.locationAppManager requestStateForRegion:(CLBeaconRegion*)region];
}
}
-(void)locationManager:(CLLocationManager *)manager didDetermineState:(CLRegionState)state forRegion:(CLRegion *)region
{
if (state == CLRegionStateInside)
{
//Start Ranging
[manager startRangingBeaconsInRegion:(CLBeaconRegion*)region];
}
}
- (void)locationManager:(CLLocationManager*)manager didEnterRegion:(CLRegion *)region
{
if ([region isKindOfClass:[CLBeaconRegion class]])
{
[self.locationAppManager startRangingBeaconsInRegion:(CLBeaconRegion*)region];
}
}
- (void)locationManager:(CLLocationManager *)manager didRangeBeacons:(NSArray *)beacons inRegion:(CLBeaconRegion *)region
{
// if([self runningInBackground])
// {
// NSLog(#" didRangeBeacons in App Delegate");
for (CLBeacon *beacon in beacons)
{
if([[DBManager getSharedInstance]shouldNotifyFor:beacon])
{
BeaconType type;
if([beacon.major integerValue]==0)
{
type = Check;
}
else
{
type = Bundle;
}
[self insert:[NSDate date]WithBeaconType:type ForBeacon:beacon];
}
}
// }
}
-(BOOL) runningInBackground
{
UIApplicationState state = [UIApplication sharedApplication].applicationState;
BOOL result = (state == UIApplicationStateBackground);
return result;
}
-(void)resetStatus
{
NSLog(#" timer has been fired");
}
-(void)entryDatabase
{
NSMutableArray *localArray = [NSMutableArray new];
BeaconProduct *bP1 = [[BeaconProduct alloc] initWithProductSku:#"1" andId:#"1" andbTitle:#"BeaconAdTitle1" andDescription:#"Description1" andCurrency:#"Rupees" andSmallImage:#"Shampoo" andLargeImage:#"Shampoo" andPrice:[NSNumber numberWithInteger:700]];
BeaconProduct *bP2 = [[BeaconProduct alloc] initWithProductSku:#"2" andId:#"1" andbTitle:#"BeaconAdTitle2" andDescription:#"Description2" andCurrency:#"Rupees" andSmallImage:#"conditionerFree" andLargeImage:#"conditionerFree" andPrice:[NSNumber numberWithInteger:200]];
BeaconProduct *bP3 = [[BeaconProduct alloc] initWithProductSku:#"3" andId:#"2" andbTitle:#"BeaconAdTitle3" andDescription:#"Description3" andCurrency:#"Rupees" andSmallImage:#"soap" andLargeImage:#"soap" andPrice:[NSNumber numberWithFloat:20.50]];
BeaconProduct *bP4 = [[BeaconProduct alloc] initWithProductSku:#"4" andId:#"3" andbTitle:#"BeaconAdTitle4" andDescription:#"Description4" andCurrency:#"Rupees" andSmallImage:#"shirt" andLargeImage:#"shirt" andPrice:[NSNumber numberWithFloat:250]];
[localArray addObject:bP1];
[localArray addObject:bP2];
[localArray addObject:bP3];
[localArray addObject:bP4];
[[DBManager getSharedInstance] insertBeaconProduct:localArray];
}
#end
You should post your code so we can explain this definitively, but it sounds like you simply have region.notifyEntryStateOnDisplay = YES; when setting up your region. When you have that option set, you will get an extra callback to your region entry callback from CLLocationManager every time you illuminate the display.
EDIT: After seeing the code, I also noticed that the app starts monitoring when it is about to to the background, then stops it when it goes back into the foreground. I suspect this may be complicating the issue and giving you extra callbacks causing extra notifications. It doesn't look like there is any reason to keep stopping and restarting monitoring. I would suggest you simply set up monitoring of your beacon regions when the app starts up, and leave monitoring going. This combined with setting region.notifyEntryStateOnDisplay=NO should prevent the multiple notifications.
SECOND EDIT: As to point (2), you should understand that you generally CANNOT range in the background at all. Once your app leaves the foreground, you can only get callbacks to didRangeBeacons: inRegion: for about five seconds, at which time such callbacks stop until the app is woken up again. One way you can wake it up is by transitioning from being in/out of a beacon region using the monitoring APIs. However, you should understand that in the background, these transitions are not always instantaneous and can take up to 15 minutes. See here for details.
I'm trying to get location updates while my app is inactive (user closed the app).
After 2 location updates, the location updates stops launching my app.
Indicator for this is the gray arrow in my app in location services settings.
What I'm trying is combination of startMonitoringSignificantLocationChanges & regionMonitoring.
I tested on iPhone 4 iOS 7.1.1 and location updates stops after 2 updates.
I tested in iPad mini WiFi+Cellular iOS 7.1.1 and location updates stops after 1 update and region monitoring send only 1 location.
Where I'm wrong?
My code:
AppDelegate.m:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);
[RegionMonitoringService sharedInstance].launchOptions = launchOptions;
[[RegionMonitoringService sharedInstance] stopMonitoringAllRegions];
if ( [CLLocationManager significantLocationChangeMonitoringAvailable] ) {
[[RegionMonitoringService sharedInstance] startMonitoringSignificantLocationChanges];
} else {
NSLog(#"Significant location change service not available.");
}
if (launchOptions[UIApplicationLaunchOptionsLocationKey]) {
[self application:application handleNewLocationEvet:launchOptions]; // Handle new location event
UIViewController *controller = [[UIViewController alloc] init];
controller.view.frame = [UIScreen mainScreen].bounds;
UINavigationController *nvc = [[UINavigationController alloc] initWithRootViewController:controller];
dispatch_async(dispatch_get_main_queue(), ^{
appDelegate.window.rootViewController = nvc;
[appDelegate.window makeKeyAndVisible];
});
}
else {
// ...
}
return YES;
}
- (void)applicationDidEnterBackground:(UIApplication *)application
{
[defaults synchronize];
[UIApplication sharedApplication].applicationIconBadgeNumber = 0;
if ([RegionMonitoringService sharedInstance].launchOptions[UIApplicationLaunchOptionsLocationKey]) {
return;
}
}
- (void)application:(UIApplication *)application handleNewLocationEvet:(NSDictionary *)launchOptions
{
NSLog(#"%s, launchOptions: %#", __PRETTY_FUNCTION__, launchOptions);
if (![launchOptions objectForKey:UIApplicationLaunchOptionsLocationKey]) return;
if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) return;
SendLocalPushNotification(#"handleNewLocationEvet");
}
RegionMonitoringService.h:
#import <Foundation/Foundation.h>
#import <CoreLocation/CoreLocation.h>
#import "ServerApiManager.h"
#interface RegionMonitoringService : NSObject
#property (strong, nonatomic) CLLocationManager *locationManager;
#property (strong, nonatomic) NSDictionary *launchOptions;
#property (strong, nonatomic) NSDate *oldDate;
#property (strong, nonatomic) CLLocation *oldLocation;
+ (RegionMonitoringService *)sharedInstance;
- (void)startMonitoringForRegion:(CLRegion *)region;
- (void)startMonitoringRegionWithCoordinate:(CLLocationCoordinate2D)coordinate andRadius:(CLLocationDirection)radius;
- (void)stopMonitoringAllRegions;
- (void)startMonitoringSignificantLocationChanges;
- (void)stopMonitoringSignificantLocationChanges;
FOUNDATION_EXPORT NSString *NSStringFromCLRegionState(CLRegionState state);
#end
RegionMonitoringService.m:
#import "RegionMonitoringService.h"
static CLLocationDistance const kFixedRadius = 250.0;
#interface RegionMonitoringService () <CLLocationManagerDelegate>
- (NSString *)identifierForCoordinate:(CLLocationCoordinate2D)coordinate;
- (CLLocationDistance)getFixRadius:(CLLocationDistance)radius;
- (void)sortLastLocation:(CLLocation *)lastLocation;
#end
#implementation RegionMonitoringService
+ (RegionMonitoringService *)sharedInstance
{
static RegionMonitoringService *_sharedInstance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedInstance = [[self alloc] init];
});
return _sharedInstance;
}
- (instancetype)init
{
self = [super init];
if (!self) {
return nil;
}
_locationManager = [[CLLocationManager alloc] init];
_locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation;
_locationManager.distanceFilter = kCLDistanceFilterNone;
// _locationManager.activityType = CLActivityTypeFitness;
_locationManager.delegate = self;
return self;
}
- (void)startMonitoringForRegion:(CLRegion *)region
{
NSLog(#"%s", __PRETTY_FUNCTION__);
[_locationManager startMonitoringForRegion:region];
}
- (void)startMonitoringRegionWithCoordinate:(CLLocationCoordinate2D)coordinate andRadius:(CLLocationDirection)radius
{
NSLog(#"%s", __PRETTY_FUNCTION__);
if (![CLLocationManager regionMonitoringAvailable]) {
NSLog(#"Warning: Region monitoring not supported on this device.");
return;
}
if (__iOS_6_And_Heigher) {
CLRegion *region = [[CLRegion alloc] initCircularRegionWithCenter:coordinate
radius:radius
identifier:[self identifierForCoordinate:coordinate]];
[_locationManager startMonitoringForRegion:region];
}
else {
CLCircularRegion *region = [[CLCircularRegion alloc] initWithCenter:coordinate
radius:radius
identifier:[self identifierForCoordinate:coordinate]];
[_locationManager startMonitoringForRegion:region];
}
SendLocalPushNotification([NSString stringWithFormat:#"StartMonitor: {%f, %f}", coordinate.latitude, coordinate.longitude]);
}
- (void)stopMonitoringAllRegions
{
NSLog(#"%s", __PRETTY_FUNCTION__);
if (_locationManager.monitoredRegions.allObjects.count > 1) {
for (int i=0; i<_locationManager.monitoredRegions.allObjects.count; i++) {
if (i == 0) {
NSLog(#"stop monitor region at index %d", i);
CLRegion *region = (CLRegion *)_locationManager.monitoredRegions.allObjects[i];
[_locationManager stopMonitoringForRegion:region];
}
}
}
}
- (void)startMonitoringSignificantLocationChanges
{
NSLog(#"%s", __PRETTY_FUNCTION__);
[_locationManager startMonitoringSignificantLocationChanges];
}
- (void)stopMonitoringSignificantLocationChanges
{
NSLog(#"%s", __PRETTY_FUNCTION__);
[_locationManager stopMonitoringSignificantLocationChanges];
}
- (NSString *)identifierForCoordinate:(CLLocationCoordinate2D)coordinate
{
NSLog(#"%s", __PRETTY_FUNCTION__);
return [NSString stringWithFormat:#"{%f, %f}", coordinate.latitude, coordinate.longitude];
}
FOUNDATION_EXPORT NSString *NSStringFromCLRegionState(CLRegionState state)
{
NSLog(#"%s", __PRETTY_FUNCTION__);
if (__iOS_6_And_Heigher) {
return #"Support only iOS 7 and later.";
}
if (state == CLRegionStateUnknown) {
return #"CLRegionStateUnknown";
} else if (state == CLRegionStateInside) {
return #"CLRegionStateInside";
} else if (state == CLRegionStateOutside) {
return #"CLRegionStateOutside";
} else {
return [NSString stringWithFormat:#"Undeterminded CLRegionState"];
}
}
- (CLLocationDistance)getFixRadius:(CLLocationDistance)radius
{
if (radius > _locationManager.maximumRegionMonitoringDistance) {
radius = _locationManager.maximumRegionMonitoringDistance;
}
return radius;
}
- (void)sortLastLocation:(CLLocation *)lastLocation
{
NSLog(#"%s, %#", __PRETTY_FUNCTION__, lastLocation);
self.oldDate = lastLocation.timestamp; // Get new date
NSTimeInterval seconds = fabs([self.oldLocation.timestamp timeIntervalSinceDate:self.oldDate]); // Calculate how seconds passed
NSInteger minutes = seconds * 60; // Calculate how minutes passed
if (lastLocation && self.oldLocation) { // New & old location are good
if ([lastLocation distanceFromLocation:self.oldLocation] >= 200 || minutes >= 30) { // Distance > 200 or 30 minutes passed
[[ServerApiManager sharedInstance] saveLocation:lastLocation]; // Send location to server
}
}
else { // We just starting location updates
[[ServerApiManager sharedInstance] saveLocation:lastLocation]; // Send new location to server
}
self.oldLocation = lastLocation; // Set old location
}
#pragma mark - CLLocationManagerDelegate Methods
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
NSLog(#"%s, %#", __PRETTY_FUNCTION__, locations);
CLLocation *lastLocation = (CLLocation *)locations.lastObject;
CLLocationCoordinate2D coordinate = lastLocation.coordinate;
if (lastLocation == nil || coordinate.latitude == 0.0 || coordinate.longitude == 0.0) {
return;
}
[self startMonitoringRegionWithCoordinate:coordinate andRadius:[self getFixRadius:kFixedRadius]];
[self sortLastLocation:lastLocation];
}
- (void)locationManager:(CLLocationManager *)manager didDetermineState:(CLRegionState)state forRegion:(CLRegion *)region
{
NSLog(#"%s, currentLocation: %#, regionState: %#, region: %#",
__PRETTY_FUNCTION__, manager.location, NSStringFromCLRegionState(state), region);
}
- (void)locationManager:(CLLocationManager *)manager didStartMonitoringForRegion:(CLRegion *)region
{
NSLog(#"%s, REGION: %#", __PRETTY_FUNCTION__, region);
[manager requestStateForRegion:region];
}
- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region
{
}
- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region
{
NSLog(#"%s, REGION: %#", __PRETTY_FUNCTION__, region);
[self stopMonitoringAllRegions];
[self startMonitoringRegionWithCoordinate:manager.location.coordinate andRadius:[self getFixRadius:kFixedRadius]];
CLLocation *lastLocation = manager.location;
CLLocationCoordinate2D coordinate = lastLocation.coordinate;
if (lastLocation == nil || coordinate.latitude == 0.0 || coordinate.longitude == 0.0) {
return;
}
[self sortLastLocation:manager.location];
}
#end
EDIT 1:
I did a a lot of real time tests with car with several devices (iPhone 5s, iPad mini, iPhone 4) after several tests I came to this:
In one case, iPad mini & iPhone 4 stops updating location after several minutes when app is not running and the little arrow become gray.
When WiFi was off, the accuracy was terrible and locations updated rarely.
EDIT 2:
OK, after a lot of driving and walking around and testing it it works like a charm so far.
I managed to make it work, combining significantLocationChanges & region monitoring, always register a geofence around my current location and always starting significant location changes when new UIApplicationLaunchOptionsLocationKey come.
Note that turning off wifi make accuracy very low and even sometimes not working.
Any bugs in my code?
From the Apple docs it can be seen that updates are not sent more frequently than every 5 minutes and for 500 meters of location change:
Apps can expect a notification as soon as the device moves 500 meters or more from its previous notification. It should not expect notifications more frequently than once every five minutes. If the device is able to retrieve data from the network, the location manager is much more likely to deliver notifications in a timely manner.
You are going to receive the updates even when the app is inactive. Another hint for you might be that oyu can test the location in the simulator instead using a real device, this way you don't have to go outside for testing and can still check your logs too. In the simulator menu, chose Debug --> Location.