An app I'm working on currently sets up region monitoring with the user's current location when the app is backgrounded. When the app becomes active again I am trying to stop monitoring for the region, but it seems to work intermittently with the majority of the time resulting in it failing to act as expected. When the app is backgrounded, I start monitoring for the region and it works fine when I log the details:
- (void)locationManager:(CLLocationManager *)manager didStartMonitoringForRegion:(CLRegion *)region {
DDLogInfo(#"CREATED REGION: %#", region.identifier);
}
Which results in the following log:
"CREATED REGION: regionFor: [real lat here, real lon here] with radius: 100"
When the app wakes up, I call the following function:
- (void)stopMonitoringAllRegions {
DDLogInfo(#"About to stop monitoring for %d regions", [locationManager monitoredRegions].count);
// stop monitoring for any and all current regions
for (CLRegion *region in [[locationManager monitoredRegions] allObjects]) {
[locationManager stopMonitoringForRegion:region];
}
DDLogInfo(#"After stopping, we're currently monitoring for %d regions", [locationManager monitoredRegions].count);
}
Which results in the following log about 75% of the time:
"About to stop monitoring for 1 regions"
"After stopping, we're currently monitoring for 1 regions"
and infrequently I get what seems like a success:
"About to stop monitoring for 1 regions"
"After stopping, we're currently monitoring for 0 regions"
I've tried a couple of things with no success. The regions I'm creating are CLCircularRegions, which inherit from CLRegion so that should work regardless, but in the for-loop I've changed CLRegion to CLCircularRegion with no effect. I was originally using [locationManager monitoredRegions] by itself, which returns an NSSet, so I thought using the allObjects function to get the array would fix the issue, but it hasn't.
I also thought it might be an issue with mutating the array while enumerating, but the only other post I saw on SO said that the above worked for them...
Am I missing something?
If you read up on monitoredRegions, it represents all monitored regions of all CLLocationManager instances, and so is probably controlled by a private dispatch queue - which would explain the delays.
My suggestion would be to keep your own mutable array (or set) around, using it to keep track of what regions are monitored and which are not, and not rely on the location manager for that collection.
Now that its clear you cannot rely on immediate changes to it, I'd design around it rather than try to find some heuristic that seems (today) to work but bites you later.
Related
In my iOS app I turned on location services in background and also set on always. I also set up region monitoring for every 500 meters so if iOS kills my app in background then it will wake up using region monitoring.
But I found one major issue in updating location. I disabled location services of iOS and re enable it, my app is still in background but it's location icon showing disable and other app which is not even in background shows enable. Please see attached screenshot.
If any one knows about this then please guide me.
I think you have wrong concept, the arrow unfilled means that the app is using geofences, if you scroll to the bottom in that list you can read the legend as you can se in this screenshot
Hope this helps you
In app delegate.m,
- (BOOL)application:(UIApplication )application didFinishLaunchingWithOptions:(NSDictionary )launchOptions
{
[self setUpLocationManager];
return YES;
}
#pragma mark - CLLocationManager Delegate
-(void)setUpLocationManager
{
LocationManager=[[CLLocationManager alloc]init];
LocationManager.delegate=self;
LocationManager.desiredAccuracy=kCLLocationAccuracyBest;
[LocationManager requestWhenInUseAuthorization];
[LocationManager startUpdatingLocation];
}
-(void)locationManager:(CLLocationManager )manager didUpdateLocations:(NSArray<CLLocation > *)locations
{
CLGeocoder *GioCoder=[[CLGeocoder alloc]init];
[GioCoder reverseGeocodeLocation:LocationManager.location completionHandler:^(NSArray<CLPlacemark > Nullable placemarks, NSError * Nullable error)
{
if (error !=nil)
{
}
else
{
CLPlacemark *place=[placemarks lastObject];
NSLog(#"%#",place);
NSString *Path=[[NSBundle mainBundle]pathForResource:#"countries" ofType:#"json"];
NSError *Error;
NSData *JsonData=[[NSData alloc]initWithContentsOfFile:Path options:NSDataReadingMappedAlways error:&Error];
NSMutableArray *DataArray=[[NSMutableArray alloc]init];
if (Error==nil)
{
NSError *err;
DataArray=[NSJSONSerialization JSONObjectWithData:JsonData options:NSJSONReadingMutableContainers error:&err];
}
for (int i=0; i<DataArray.count;i++)
{
if ([[[DataArray objectAtIndex:i]valueForKey:#"code"] isEqualToString:place.ISOcountryCode])
{
// Insert your code here
[LocationManager stopUpdatingLocation];
break;
}
}
}
}];
}
-(void)locationManager:(CLLocationManager )manager didFailWithError:(NSError )error
{
if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined || [CLLocationManager authorizationStatus] == kCLAuthorizationStatusDenied || [CLLocationManager authorizationStatus] == kCLAuthorizationStatusRestricted)
{
NSLog(#"Location not enabled in your device");
}
else
{
}
}
Just as Reiner said in his answer, your understanding is wrong. If you have an empty purple arrow it means your geofence is set correctly.
If another app is staying filled purple. It could be because of multiple reasons:
It has pausesLocationUpdatesAutomatically set to false and is always tracking.
It has pausesLocationUpdatesAutomatically set to true but hasn't yet paused long enough at a location to trigger a pause which would technically turn that filled purple into either gray or empty purple (which means the app is continuing tracking using significant location changes or regionMonitoring or visits monitoring)
AFAIK if you region is setup for 500meters then it's better to use SignificantLocation Changes. Because you can use cellTower information. For regionMonitoring app turns it's location Tracking on and off every few minutes to make sure it's been exited the app or not.
VERY IMPORTANT NOTE
Apple documents on this are very confusing. It took me a whole week to figure it out!
Region Monitoring or significant location Tracking or visit Monitoring will only launch app in the background if and only if your app was terminated. If your app is launched again then right from the DidFinishLaunching you can startLocationUpdates.
If your app was suspended or for some reason still in the background you would only get the didExitRegion callback. And AFIAK from the callback you're not able to startUpdateLocations. This is a very vague text from WWDC.
Finally, you must start your location updates while in the
foreground. If you don’t start your updates in the foreground, you
won’t get this special behavior. So what happens if you do start your
updates in the background? Well, first off, your app will probably
need AlwaysAuthorization since your app won’t be considered in use at
that time. Furthermore, Core Location won’t take any action to
ensure your app continues to run. So if you have background runtime
for some reason, and you decide to start a location session, you
might get some updates, but you might also get suspended before you receive all the information you had hoped to receive.
WWDC 2016: Session 716
I don't know why Apple is saying might...it could be something that varies based on battery/cell coverage/ usage patterns / iOS version. It's obviously something they don't want us developers know how. They want to force us to go for other ways.
The only way for you to get your app into terminated state is either if you user manually kills the app or you wait for a very very long time ie wait for any or all other background tasks to finish + then once app is suspended wait for it to become terminated and then exit the region to launch app and be able to start tracking Locations
Below are some snippets of my code
BOOL checkingForLocation;
.
.
.
- (IBAction)LocationButtonTapped:(id)sender {
[locationManager startUpdatingLocation];
checkingForLocation = YES;
}
.
.
.
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations {
[locationManager stopUpdatingLocation];
// locationManager = nil;
if (checkingForLocation) {
checkingForLocation = NO;
NSLog(#"here");
// fetch data from server using location and present view controllers
}
}
didUpdateLocations is called multiple times because locationManager is continually updating the location per startUpdatingLocation. My app has a bug where the code within if(checkingForLocation) runs multiple times and renders the view multiple times causing undesired results.
1) Why is it that stopUpdatingLocation doesn't stop further calls to didUpdateLocations?
2) Furthermore, if the checkingForLocation has already been set to NO why does the subsequent calls to didUpdateLocations still treat checkingForLocation as YES. Is there a race condition there?
Through searching other SO questions, I found that setting locationManager = nil will stop the extra calls to didUpdateLocations, which it does fix the issue in my case, but no real explanation. It seems that stopUpdatingLocation should take care of that. Any insight is much appreciated.
Thank you,
didUpdateLocations: can be called frequently and at any time because of its async nature and optimisations :
Because it can take several seconds to return an initial location, the
location manager typically delivers the previously cached location
data immediately and then delivers more up-to-date location data as it
becomes available. Therefore it is always a good idea to check the
timestamp of any location object before taking any actions. If both
location services are enabled simultaneously, they deliver events
using the same set of delegate methods.
Actually, if you uncomment locationManager = nil; it should help, but if you want to use your checkingForLocation property, you should make the assignment synchronised. #synchronized(self) is the easiest way in this case.
I've set up Google Nearby API for my objective-c project to scan for beacons.
The app detects the beacons fine when moving into the range of a beacon but it does't work if I start the app when I'm already in range. I have to walk away from the beacon and return.
I am not using background scanning. The lib version I use is: 0.10.0
My code is:
[GNSMessageManager setDebugLoggingEnabled:YES];
_messageManager = [[GNSMessageManager alloc] initWithAPIKey:#"..."];
_beaconSubscription = [_messageManager subscriptionWithMessageFoundHandler:^(GNSMessage *message) {
NSLog(#"beacon found: %#",message);
...
} messageLostHandler:^(GNSMessage *message) {
NSLog(#"beacon lost: %#",message);
...
} paramsBlock:^(GNSSubscriptionParams *params) {
params.deviceTypesToDiscover = kGNSDeviceBLEBeacon;
params.beaconStrategy = [GNSBeaconStrategy strategyWithParamsBlock:^(GNSBeaconStrategyParams *params) {
params.includeIBeacons = YES;
}];
}];
I know about the Core Location Framework didEnterRegion / didExitRegion methods that are called only when crossing the boundaries of a beacon region and that I can use didDetermineState method but how does the NearbyAPI work on the inside with these and how can I make the app detect the beacons already in range at startup using it?
This is indeed a bug in the way Nearby Messages monitors iBeacon regions. It uses didEnterRegion/didExitRegion, and as you stated, if you're already in a region when scanning starts, didEnterRegion isn't called.
I've experimented with using didDetermineState, and with a bit of work I'm now able to handle this case. We will include this in the next bug fix release.
In the meantime, here's a trick you can use to avoid the problem while testing your app: Put your beacon into a metal enclosure (a faraday cage), and remove it from the enclosure after your app starts scanning for beacons. This simulates movement into the beacon region. I use a small cocktail shaker for my faraday cage, but a small amount of aluminum foil also works.
On iOS, in my application delegate I start region monitoring and as soon as I enter in a beacon region I start the ranging logic, using locationManager:didRangeBeacons:inRegion. According to the Apple documentation, this method should be called only when the the region comes within the range or out of the range or when the range changes.
My problem is that I get a call to this method every second as long as I am inside the region. How to decrease the number of calls to this method while still ranging?
locationManager:didRangeBeacons:inRegion is called once per second, no matter what. Each time it's called, the beacons parameter will contain an array of all beacons that the app can currently see, ordered by proximity. There's no way to limit the frequency at which this method is called, short of stopping ranging.
When monitoring regions (instead of ranging), your app will have didEnterRegion: and didExitRegion called, along with didDetermineState:. See this answer for a little more detail.
According to the Docs:
"The location manager calls this method whenever a beacon comes within range or goes out of range. The location manager also calls this method when the range of the beacon changes; for example, when the beacon gets closer."
Whats probably happening is the range is changing slightly which is causing the behaviour you describe.
Why is this a problem
EDIT:
IN the background you will get notified of entering regions via the app delegate method:
- (void)locationManager:(CLLocationManager *)manager didDetermineState:(CLRegionState)state forRegion:(CLRegion *)region{}
You can use this to determine the state:
if(state == CLRegionStateInside)
{
//Inside a region:
}
else if(state == CLRegionStateOutside)
{
//Outside a region
}
else {
//Something else
}
You can use this to gather a limited amount of information or prompt the user to load the application via a local notification. When your app resumes you can then gather more information via the locationManager.
in my IOS app I am implementing geofencing. In the current implementation I am using code like this:
CLRegion* region3 = [[CLRegion alloc] initCircularRegionWithCenter:coordinates radius:100 identifier:#"region3"];
[self.locationManager startMonitoringForRegion:region desiredAccuracy:kCLLocationAccuracyHundredMeters];
and then I am using these delegate methods:
(void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region{
NSLog(#"didenterregion");
}
(void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region{
NSLog(#"didexitregion");
}
(void)locationManager:(CLLocationManager *)manager monitoringDidFailForRegion:(CLRegion *)region withError:(NSError *)error
{NSLog(#"monitoringDidFailForRegion");}
However, this code works fine only for radius bigger than 100m.
Here are some questions:
Apple says that in ios6 and above a radius between 1 and 400m is supported for devices 4s and above. Since I do not care how long it will take for the message to be viewed (like I do not care to see the message when entering the region but I do care to see at a latter if I have passed from that region once) can I use smaller radius? I am interested in something like 50m radius or smaller? (in some regions even 20m will be needed for my case).
I have also think that. Apple says that up to 20 regions could be supported. What are the advantages/ disadvantages of a sollution like this (I haven't implemented it yet but I want your opinion).
The pseudocode would be like this:
Declare the regions - save them in an array
Do not call start monitoring
And then in the delegate method:
- (void)locationManager:(CLLocationManager *)manager
didUpdateToLocation:(CLLocation *)newLocation
fromLocation:(CLLocation *)oldLocation
{
for loop in all my regions {
if ([region containsCoordinate: newLocation.coordinate])
code for entering region
}
}
Would it be slower?
Would it consume more battery? (I think monitoring for regions is not battery consuming)?
Could it be more accurate?
Can I have more than 20 regions since I am not registering for monitor?
Thanks in advance.
1.
I suspect the second (didUpdateToLocation:-based) implementation would be more expensive (in terms of battery life) versus the first implementation, simply because you would only run the code in the first (startMonitoringForRegion:-based) implementation if and only if the device came within the radius of one of the (maximum of 20) regions you're tracking.
Whereas in the second implementation, code has to run each time there's a "didUpdateToLocation:" delegate call (which will happen fairly often) and then the code inside the delegate method will get run.
B.T.W., you say the code works fine for a radius of 100m of above, but then the Apple documentation says it should work in iOS6 with "a radius between 1 and 400m is supported for devices 4s and above."
Is your "100m" number your practical result or is it a limitation of the device you're using (something older than an iPhone 4s or an older iOS version)?
2.
Doing anything in the background does consume battery but Apple has optimized CoreLocation for this somewhat (provided you set the correct flag in your app's info.plist file)
3.
I think both will be about equally accurate, except for the fact it may take up to a few minutes for "startMonitoringForRegion:" to report that the region was entered or exited.
4.
And yeah, in the second implementation, you can have as many regions as you want to track. However, the more code you run in the background, the hotter the battery gets and the more likely it is you're draining the battery faster.