I'm developing an iOS app based on the map and location tracking. When the user first boots the app it asks for permission to track location etc. just fine. The only problem is while it's doing that, I have code setting up the initial map view and other location-based variables before the user has clicked OK.
I've found I can put these initiation steps after a while loop that waits for the user to change the location manager's permissions as below but this can't possibly be best practice, not to mention it leads to some strange behavior in the transition between the splash screen and the map:
BOOL firstrun = TRUE;
while ([[locationManager class] authorizationStatus] == kCLAuthorizationStatusDenied || [[locationManager class] authorizationStatus] == kCLAuthorizationStatusNotDetermined) {
NSLog(#"Waiting for location permission");
}
...initiation code...
Is there a "location access granted" listener for the alert box or a similar function in the location manager delegate I don't know about? I see no such method in the docs. Anyone know what the best practice is here? Thank you so much.
EDIT
I start my location tracking as follows:
if (nil == locationManager)
locationManager = [[CLLocationManager alloc] init];
[locationManager startMonitoringSignificantLocationChanges];
self.musicmap.delegate = self;
[self.mymap setShowsUserLocation:true];
Thanks
I would recommend making your class a CLLocationManagerDelegate and then implementing this method:
- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
//Your code goes here
}
More information about CLLocationManagerDelegate can be found here.
Hope that helps!
I had a similar problem with my application and the app doing things before the user has time to accept or decline the location permission dialogue. Here is what I ended up doing.
-(BOOL)locationAuthorizationStatus {
if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusDenied) {
// user has not authorized us to use location
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:NSLocalizedString(#"Location Denied", #"Location Denied")
message:NSLocalizedString(#"This app does not have permission to access your location. Please enable location access in device settings.", #"Message stating this app does not have permission to access your location and to enable location permission in settings")
delegate:self
cancelButtonTitle:NSLocalizedString(#"Ok", #"Ok")
otherButtonTitles: nil];
[alert show];
return NO;
}
// Check if region monitoring is available for this device
if (![CLLocationManager regionMonitoringAvailable]) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:NSLocalizedString(#"Geofencing Unavailable", #"Geofencing Unavailable")
message:NSLocalizedString(#"This device is not able to monitor regions", #"Message stating this device is not able to monitor regions")
delegate:nil
cancelButtonTitle:nil
otherButtonTitles:NSLocalizedString(#"Ok", #"Ok"), nil];
[alert show];
return NO;
} else {
if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined) {
// trigger a location check to prompt user for authorization
LocationManagerController *locationController = [LocationManagerController sharedManager];
[locationController.locationManager startUpdatingLocation];
// the dialogue box is triggered here
[locationController.locationManager stopUpdatingLocation];
_waitingOnAuthorization = YES;
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(checkForAuthorizationStatusChange) name:#"WaitingOnAuthorizationStatus" object:nil];
return NO;
}
}
return YES;
}
-(void)checkForAuthorizationStatusChange {
if (_waitingOnAuthorization) {
// this should only catch location change on first time
if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorized) {
// user approved location services
} else {
// user declined authorization
}
// set flag back to NO
_waitingOnAuthorization = NO;
}
// remove our notification observer
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
You will have to add the variables that apply to your use case. But here is the gist of it.
Check for authorization status via BOOL method (locationAuthorizationStatus)
If ok to use location, returns YES do whatever you want
If not, returns NO and alerts user
If first time, will fire up the location manager to trigger location dialogue, then stop it to save battery, sets the flag and sets a notification so you will know when the user has hit yes or no.
The notification fires the method checkAuthorizationChangeStatus and rechecks permissions to see what the user did. From there, you can call any methods you need based on the users choice.
Apple does not have any delegate methods to catch this selection, so the only way around it is kinda of hacky. This method has worked well for me. Slightly hacky, but works. Hope this helps.
//Start up motion manager, not sure if you need this for location manager
motionManager = [[CMMotionManager alloc] init];
if (motionManager.accelerometerAvailable) {
motionManager.accelerometerUpdateInterval = 1.0/2.0;
[motionManager startAccelerometerUpdates];
}
locationManager = [[CLLocationManager alloc] init];
//We will be the location manager delegate
locationManager.delegate = self;
//Track position at the 100m accuracy
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters;
//We want to see all location updates, regardless of distance change
locationManager.distanceFilter = 0.0;
[locationManager startUpdatingLocation];
Put the above somewhere in your "viewDidLoad" or appDelegate "didFinishLaunchingWithOption"
The following handles the updates and stuff. bestLocation is a CLLocation. You'll need to implement CLLocationManagerDelegate. TWO_MINUTES = 120
- (BOOL)isBetterLocation:(CLLocation *)location {
if (bestLocation == nil){
//best location not set yet, so it's a better location by default
return YES;
}
// Figure out how long it's been since we got a better location
NSTimeInterval timeDelta = [location.timestamp timeIntervalSinceDate:bestLocation.timestamp];
BOOL isSignificantlyNewer = timeDelta > TWO_MINUTES;
BOOL isSignificantlyOlder = timeDelta < -TWO_MINUTES;
BOOL isNewer = timeDelta > 0;
if (isSignificantlyNewer) {
return YES;
}else if (isSignificantlyOlder) {
return NO;
}
CLLocationAccuracy accuracyDelta = location.horizontalAccuracy - bestLocation.horizontalAccuracy;
//You want accuracy to be low
BOOL isLessAccurate = accuracyDelta > 0;
BOOL isMoreAccurate = accuracyDelta < 0;
BOOL isDifferent = location.coordinate.latitude != bestLocation.coordinate.latitude ||
location.coordinate.longitude != bestLocation.coordinate.longitude;
if (isMoreAccurate) {
return YES;
} else if (isNewer && !isLessAccurate && isDifferent) {
return YES;
}
return NO;
}
#pragma mark - Location manager delegate
- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation {
if ([self isBetterLocation:newLocation]){
self.bestLocation = newLocation;
} else {
[locationManager stopUpdatingLocation];
isLocating = NO;
NSLog(#"AppDelegate: Turning off location manager >>>>>>>>>>>>>>>>>>>>>");
}
}
Note: You don't need the "isBetterLocation" method. That will just check the location and turn off the location manager after like 2 minutes so you don't wear down the user's battery. Sometimes you don't want the manager off though, so you can leave it out if you want.
I don't know how much of this you've done already. This is the way I did it.
Related
Can I detect whether a user moved to another country?
(Not using Locale.current)
The location detection should be running in background.
I'm hoping to do something like this.
Eg. A user from US leaves the country to UK. Then, when the user reach UK, i am able to detect it at the background and send notification.
You should in your Info.plist, set allowsBackgroundLocationUpdates to YES, this you can search google, and lots of answer for adapt iOS 9.
First you can use CLLocationManager to get the location:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib .
//delegate
self.locationManager.delegate = self;
//The desired location accuracy.
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
//Specifies the minimum update distance in meters.
self.locationManager.distanceFilter = kCLDistanceFilterNone;
self.locationManager.purpose = #"To provide functionality based on user's current location.";
[self.locationManager startUpdatingLocation];
}
- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation{
UIAlertView* av = [[UIAlertView alloc] initWithTitle:#"update" message:[NSString stringWithFormat:#"didUpdateToLocation: newLocation: %# old:%#",newLocation,oldLocation] delegate:nil cancelButtonTitle:#"cancel" otherButtonTitles:#"ok", nil nil];
[av show];
}
Secondly, you can use CLGeocoder to get country or city.
- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation
{
// get city name
CLGeocoder *geocoder = [[CLGeocoder alloc] init];
[geocoder reverseGeocodeLocation:newLocation completionHandler:^(NSArray *array, NSError *error)
{
if (array.count > 0)
{
CLPlacemark *placemark = [array objectAtIndex:0];
NSString *city = placemark.locality;
}
else if (error == nil && [array count] == 0)
{
NSLog(#"No results were returned.");
}
else if (error != nil)
{
NSLog(#"An error occurred = %#", error);
}
}];
}
You can give a duration to get location per duration:
-(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations{
newLocation = [locations lastObject];
double lat = newLocation.coordinate.latitude;
double lon = newLocation.coordinate.longitude;
NSLog(#"lat:%f,lon:%f",lat,lon);
if (!self.deferringUpdates) {
CLLocationDistance distance = 500;
NSTimeInterval time = 20;
[locationManager allowDeferredLocationUpdatesUntilTraveled:distance
timeout:time];
self.deferringUpdates = YES;
}
}
You can create UNNotificationRequest with exit UNLocationNotificationTrigger.
UNNotificationRequest
A UNNotificationRequest object is used to schedule a local notification and manages the content for a delivered notification. A notification request object contains a UNNotificationContent object with the contents of the notification. It also contains the UNNotificationTrigger object that specifies the conditions that trigger the delivery of the notification. For a delivered notification, you use these objects to fetch information about the notification.
UNLocationNotificationTrigger
A UNLocationNotificationTrigger object causes the delivery of a notification when the device enters or leaves a specified geographic region. Use this object to specify the region information needed to trigger the notification. Location triggers can fire once or they can fire multiple times.
Apps must request access to location services and must have when-in-use permissions to use this class. To request permission to use location services, call the requestWhenInUseAuthorization() method of CLLocationManager before scheduling any location-based triggers.
Flow
Each time user opens app, check his local country and define location trigger
let region: CLRegion = <your code defining country region>
region.notifyOnEntry = false
region.notifyOnExit = true
let trigger = UNLocationNotificationTrigger(region: region, repeats: false)
and using that trigger reschedule notification request (UNNotificationRequest).
When trigger fires (user leaves region) — app will present local notification, and if user taps on it, app starts, and if you add handler on local notification open you can notify your server about user moving away and check his new country and do what you need to do.
I want call startMonitoringForRegion in method didChangeAuthorizationStatus and everything works fine, basically because code is copied from Estimote Example. This code is in ViewController. Issue occurs when I'm using similar code but placed in separated controller.
This code works fine and method didChangeAuthorizationStatus also didStartMonitoringForRegion is called every time so I open app or changeStatus. So success.
ViewController.m
- (void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
self.region = [[CLBeaconRegion alloc] initWithProximityUUID:[[NSUUID alloc] initWithUUIDString:#"myUUID"]
identifier:#"EstimoteSampleRegion"];
self.beaconManager = [[ESTBeaconManager alloc] init];
self.beaconManager.delegate = self;
[self startRangingBeacons];
}
-(void)startRangingBeacons
{
if ([ESTBeaconManager authorizationStatus] == kCLAuthorizationStatusNotDetermined)
{
[self.beaconManager requestAlwaysAuthorization];
}
else if([ESTBeaconManager authorizationStatus] == kCLAuthorizationStatusDenied)
{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Location Access Denied"
message:#"You have denied access to location services. Change this in app settings."
delegate:nil
cancelButtonTitle:#"OK"
otherButtonTitles: nil];
[alert show];
}
else if([ESTBeaconManager authorizationStatus] == kCLAuthorizationStatusRestricted)
{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Location Not Available"
message:#"You have no access to location services."
delegate:nil
cancelButtonTitle:#"OK"
otherButtonTitles: nil];
[alert show];
}
}
- (void)beaconManager:(id)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status
{
if (status != kCLAuthorizationStatusNotDetermined && status != kCLAuthorizationStatusDenied )
{
[self.beaconManager startMonitoringForRegion:self.region];
}
}
- (void)beaconManager:(id)manager didStartMonitoringForRegion:(CLBeaconRegion *)region{
NSLog(#"didStartMonitoringForRegion %#",region);
}
This code works partially. Method requestAlwaysAuthorization in BeaconManager is calling once but then didChangeAuthorizationStatus is not even when I've manually called startMonitoringForRegion then method didStartMonitoringForRegion is not called.
Separeted BeaconManager
ViewController.m
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
self.region = [[CLBeaconRegion alloc] initWithProximityUUID:[[NSUUID alloc] initWithUUIDString:#"myUUID"]
identifier:#"EstimoteSampleRegion"];
self.beaconManager = [[BeaconManager alloc]initWithRegion:self.region];
[self.beaconManager startMonitoring];
}
BeaconManager.swift
class BeaconManager: NSObject,ESTBeaconManagerDelegate {
private lazy var secureBeaconManager:ESTSecureBeaconManager = {
let beaconManagerForReturn = ESTSecureBeaconManager()
beaconManagerForReturn.delegate = self
return beaconManagerForReturn
}()
private lazy var beaconsArray = [CLBeacon]()
var region:CLBeaconRegion!
var delegate:BeaconDelegate?
init(region:CLBeaconRegion) {
super.init()
self.region = region
}
/**
Method which check AuthorizationStatus for application, and will start monitoring if status is equal .AuthorizedAlways
*/
func startMonitoring() {
setupLocalizationAuthorization()
}
private func setupLocalizationAuthorization(){
let status = ESTSecureBeaconManager.authorizationStatus()
if (status == .NotDetermined){
secureBeaconManager.requestAlwaysAuthorization()
}
if (status == .Denied){
delegate?.beaconManager(self, locationAutorizationFail: .Denied)
}
if (status == .Restricted){
delegate?.beaconManager(self, locationAutorizationFail: .Restricted)
}
}
// MARK: - BeaconManager
func beaconManager(manager: AnyObject, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
if (status != .NotDetermined && status != .Denied){
secureBeaconManager.startMonitoringForRegion(region)
}
}
func beaconManager(manager: AnyObject, didStartMonitoringForRegion region: CLBeaconRegion) {
print("didStartMonitoring for \(region)")
}
}
Question
Why this is happening? It's thread issue? I have no clue what's going wrong here. From I can see both codes are similar but instructions are placed in different place. It's no matter If I use Swift or Obj-c (tried it already).
Analyze what happens when you start the app for the first time:
You instantiate your BeaconManager and call startMonitoring, which in turn calls setupLocalizationAuthorization. In there, you check the authorization status of your app, and if it's NotDetermined, you instantiate secureBeaconManager (because you declared it a lazy property), assign the delegate (via your lazy-instantion code), and call requestAlwaysAuthorization. This prompts the user to allow the app to access Location Service, and if they agree, iOS issues a call to didChangeAuthorizationStatus with the new, Always status, and you finally you start monitoring. This flow, as you mentioned, works as intended.
Consider however the flow where the app already has the Always authorization:
You instantiate your BeaconManager and call startMonitoring, which in turn calls setupLocalizationAuthorization. In there, you check the authorization status of your app. But you have no if clause for Always status … so the BeaconManager simply finishes there.
Solution: append an if clause for the Always status to your setupLocalizationAuthorization:
if status == .AuthorizedAlways {
secureBeaconManager.startMonitoringForRegion(region)
}
Let's do one final pass over what happens when you implement the proposed solution:
You instantiate your BeaconManager and call startMonitoring, which in turn calls setupLocalizationAuthorization. In there, you check the authorization status of your app. If it's Always, you lazy-instantiate the secureBeaconManager, and start monitoring. The previous flow (for NotDetermined) stays the same.
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.
I am writing both Android and iOS apps which need to find BLE beacons around the device.
When I run my code from Android, it finds several beacons in the room I am in.
I have 8 beacons.
When I run the beacon code from iPhone, it returns a list of exactly 0 beacons.
Here is my code:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
[self initRegion];
[self locationManager:self.locationManager didStartMonitoringForRegion:self.beaconRegion];
}
- (void)locationManager:(CLLocationManager *)manager didStartMonitoringForRegion:(CLRegion *)region {
[self.locationManager startRangingBeaconsInRegion:self.beaconRegion];
}
- (void)initRegion {
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:BEACONUUID];
self.beaconRegion = [[CLBeaconRegion alloc] initWithProximityUUID:uuid identifier:BEACONIDENTIFIER];
[self.locationManager startMonitoringForRegion:self.beaconRegion];
}
- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region {
NSLog(#"Beacon Found");
[self.locationManager startRangingBeaconsInRegion:self.beaconRegion];
}
-(void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region {
NSLog(#"Left Region");
[self.locationManager stopRangingBeaconsInRegion:self.beaconRegion];
self.beaconFoundLabel.text = #"No";
}
-(void)locationManager:(CLLocationManager *)manager didRangeBeacons:(NSArray *)beacons inRegion:(CLBeaconRegion *)region {
CLBeacon *beacon = [beacons lastObject];//<== is always 0
self.beaconFoundLabel.text = #"Yes";
self.proximityUUIDLabel.text = beacon.proximityUUID.UUIDString;
self.majorLabel.text = [NSString stringWithFormat:#"%#", beacon.major];
self.minorLabel.text = [NSString stringWithFormat:#"%#", beacon.minor];
self.accuracyLabel.text = [NSString stringWithFormat:#"%f", beacon.accuracy];
if (beacon.proximity == CLProximityUnknown) {
self.distanceLabel.text = #"Unknown Proximity";
} else if (beacon.proximity == CLProximityImmediate) {
self.distanceLabel.text = #"Immediate";
} else if (beacon.proximity == CLProximityNear) {
self.distanceLabel.text = #"Near";
} else if (beacon.proximity == CLProximityFar) {
self.distanceLabel.text = #"Far";
}
self.rssiLabel.text = [NSString stringWithFormat:#"%ld", (long)beacon.rssi];
}
In my didRangeBeaconsInRegion, the beacons NSArray always comes up with 0 objects.
Though I have 8 objects. And i've downloaded several apps that are not mine, and they all see several of my beacons.
Why doesn't my code see any of my beacons?
Here is what I do whenever I'm setting up an iBeacon app.
Not all these things are necessary, but it will
work
keep your user happy
(maybe most importantly) keep Apple happy
iOS 8+ Only
First things first: if you're using iOS 8, you need to make sure you actually have access before using CLLocationManager.
CLLocationManager *locationManager = [[CLLocationManager alloc] init];
locationManager.delegate = self;
// You can either use requestAlwaysAuthorization, or requestWhenInUseAuthorization
[self.locationManager requestAlwaysAuthorization];
You'll also need an entry for NSLocationAlwaysUsageDescription in your plist (again, iOS 8 only )
iOS 7+
Your App's pList
Regardless you're using iOS 8 or 7, you should add the following to your plist file (you need to decide if you'll use background or not).
Note: The below is in the form of KEY : KEY TYPE : VALUE for string, and KEY : KEY TYPE : [ Value1, Value2... ] for Arrays:
NSLocationUsageDescription : String : "Gimmie access to your location or else..."
NSBluetoothPeripheralUsageDescription : String : "Gimmie access to your blue tooth or else"
// Optionally supply if you need background modes. I don't believe you need this either if you plan to turn these options on using the Capabilities section of your App's Settings (see below section)
UIBackgroundModes : Array : [ location, bluetooth-central ]
UIApplicationExitsOnSuspend : Boolean : NO
Your App's Project Settings (Capabilities)
this section has been removed as this can cause your app to be rejected (as noted by #heypiotr in the comments)
Final Thoughts
A final suggestion would be to try moving [self locationManager:self.locationManager didStartMonitoringForRegion:self.beaconRegion] into your viewDidAppear.
Here is an example of what I typically do ( which works quite well for me ).
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self initLocationManager];
[self initBeaconRegion];
[self startMonitoring];
}
-(void)initLocationManager {
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
// Not necessary, but I like to do it.
if( ![CLLocationManager locationServicesEnabled] ) {
[self.locationManager startUpdatingLocation];
}
// Only necessary if you're in iOS 8. Checking for existence though to support iOS 7
if( ![CLLocationManager authorizationStatus] != kCLAuthorizationStatusAuthorized ) {
if ([CLLocationManager respondsToSelector:#selector(requestAlwaysAuthorization)]) {
[self.locationManager performSelector:#selector( requestAlwaysAuthorization )];
}
}
}
-(void)initBeaconRegion {
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:kYOUR_UUID_HERE];
self.beaconRegion = [[CLBeaconRegion alloc] initWithProximityUUID:uuid identifier:kYOUR_IDENTIFIER_HERE];
// Change this to whatever you want....
self.beaconRegion.notifyEntryStateOnDisplay = YES;
self.beaconRegion.notifyOnEntry = NO;
self.beaconRegion.notifyOnExit = YES;
}
# pragma mark -
# pragma mark Monitoring Beacons
# pragma mark -
-(void)startMonitoring {
// Monitor Beacon signals around me and report them back to me
[self.locationManager startMonitoringForRegion:self.beaconRegion];
}
The last part ( placement in viewDidAppear ) may or may not help - it all depends I guess on too many factors to consider in a stackoverflow response.
Hope that helps and good luck!
Final Edit
Forgot one more thing that may help. There are some helpful methods that you can implement that can help you debug the issue further. Here is an example of a few of them:
#pragma mark Authorization Status Changed
-(void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
if( ![CLLocationManager locationServicesEnabled] ) {
NSLog(#"Couldn't turn on ranging: Location services are not enabled.");
} else {
NSLog(#"Location services ARE enabled.");
}
if( [CLLocationManager authorizationStatus] != kCLAuthorizationStatusAuthorized ) {
NSLog(#"Couldn't turn on monitoring: Location services not authorized.");
} else {
NSLog(#"Location services ARE authorized.");
}
}
#pragma mark -
#pragma mark CLLocationManager Errors
#pragma mark -
-(void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error {
NSLog( #"FAIL ERROR: %#", [error description] );
}
-(void)locationManager:(CLLocationManager *)manager rangingBeaconsDidFailForRegion (CLBeaconRegion *)region withError:(NSError *)error {
NSLog( #"RANGE BEACONS ERROR: %#", [error description] );
}
First check with the other app like.Locate app add your UDID and check this app is showing your iBeacon. if not then there is a problem with apple IOS sometimes. then remove the app. Restart the app. it will work for you.
We had similar problem before, make sure your iBeacon device respond the scan request from iOS with a response DOES not contain manufacturer specific field.
Please check this note from apple
Before attempting to monitor any regions, your app should check whether region monitoring is supported on the current device. Here are some reasons why region monitoring might not be available:
The device doesn’t have the necessary hardware to support region
monitoring.
The user denied the app the authorization to use region monitoring.
The user disabled location services in the Settings app.
The user disabled Background App Refresh in the Settings app, either for the device or for your app.
5.The device is in Airplane mode and can’t power up the necessary hardware.
In iOS 7.0 and later, always call the isMonitoringAvailableForClass: and authorizationStatus class methods of CLLocationManager before attempting to monitor regions. (In OS X v10.8 and later and in previous versions of iOS, use the regionMonitoringAvailable class instead.) The isMonitoringAvailableForClass: method tells you whether the underlying hardware supports region monitoring for the specified class at all. If that method returns NO, your app can’t use region monitoring on the device. If it returns YES, call the authorizationStatus method to determine whether the app is currently authorized to use location services. If the authorization status is kCLAuthorizationStatusAuthorized, your app can receive boundary crossing notifications for any regions it registered. If the authorization status is set to any other value, the app doesn’t receive those notifications.
Apple link goes here
I have an app which uses the device location. If they allow the location, I want to run my method getDataFromJson and run my app as normal. If they deny it, or have denied it before, I wish to show them a view explaining they need to go to settings and allow it.
I have a lot of code, but it doesn't work at the moment. Can anyone help explain where the problem is?
Many thanks!
- (void)viewDidLoad
{
[super viewDidLoad];
if ([CLLocationManager authorizationStatus] == YES) {
//are enabled, run the JSON request
[self getDataFromJson];
} else {
//is not enabled, so set it up
NSLog(#"no");
[locationManager location];
};
}
-(CLLocationCoordinate2D) getLocation{
locationManager = [[CLLocationManager alloc] init];
locationManager.delegate = self;
locationManager.desiredAccuracy = kCLLocationAccuracyBest;
locationManager.distanceFilter = kCLDistanceFilterNone;
[locationManager startUpdatingLocation];
CLLocation *location = [locationManager location];
CLLocationCoordinate2D coordinate = [location coordinate];
return coordinate;
}
-(void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
if (status == kCLAuthorizationStatusDenied) {
//location denied, handle accordingly
locationFailView.hidden = NO;
mainView.hidden = YES;
}
else if (status == kCLAuthorizationStatusAuthorized) {
//hooray! begin tracking
[self getDataFromJson];
}
}
//class to convert JSON to NSData
- (IBAction)getDataFromJson {
CLLocationCoordinate2D coordinate = [self getLocation];
NSString *latitude = [NSString stringWithFormat:#"%f", coordinate.latitude];
NSString *longitude = [NSString stringWithFormat:#"%f", coordinate.longitude];
...
}
+ (CLAuthorizationStatus)authorizationStatus
Return Value A value indicating whether the application is authorized
to use location services.
Discussion The authorization status of a given application is managed
by the system and determined by several factors. Applications must be
explicitly authorized to use location services by the user and
location services must themselves currently be enabled for the system.
This authorization takes place automatically when your application
first attempts to use location services.
+ (BOOL)locationServicesEnabled
Returns a Boolean value indicating whether location services are
enabled on the device.
You may check this two state: locationServicesEnabled and authorizationStatus then decide which method should use.
AuthorizationStatus should check with states:
typedef enum {
kCLAuthorizationStatusNotDetermined = 0,
kCLAuthorizationStatusRestricted,
kCLAuthorizationStatusDenied,
kCLAuthorizationStatusAuthorized
} CLAuthorizationStatus;
but you check equal with bool value in viewDidLoad method.