I want to check every time the user passes a store from an array of stores, I have more than 20 stores so I wrote a function that finds the 20 closest stores to the user’s location and monitors them. The list being updated on locationManager: didUpdateLocations
, I also replace the old 20 monitored regions with the new 20 closest stores location.
The problem is that locationManager: didEnterRegion
isn’t being called regularly when the user enters a region.
I’ve also noticed that when I check the locationManager.monitoredRegions
NSSet
the regions there are wrong for some reason (I checked it with if
sentence so maybe they are correct and just shorter?).
If someone could check my code and maybe notice something I done wrong, It’ll really help me!
My code:
monitorLocationViewController.m(scroll to see the full code):
-(void)getStoresArrays:(NSNotification*)notification
{
//Fetching "allStoresArray"(NSArray of all the stores sent from another class using NSNotificationCenter) and adding it to "allStores"(NSMutableArray)
NSDictionary *storesCategoriesArrays=[notification userInfo];
self.allStores=[storesCategoriesArrays objectForKey:@"allStoresArray"];
//Calling "locationChangeHandler" for monitoring
[self locationChangeHandler];
}
#pragma mark - CLLocationDelegate methods
//Being called when user's location updated
-(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations
{
//If "allStores"(NSMutableArray) isn't nil - calling "locationChangeHandler" to update monitoring
if (self.allStores!=nil) {
[self locationChangeHandler];
}
}
//Being called when user enters a monitored region
-(void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region
{
NSLog(@"Entered");
}
#pragma mark - Closest stores sorting methods
//Sorting closest stores to the user's location and adding the 20 closest store to "twentyClosestStores"(NSMutableArray)
-(void)sortClosestStores
{
//Sorting "allStores"(NSMutableArray) from the closest "Store" to the furthest.
[self.allStores sortUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) {
//Creating "location1"(CLLocation) and "location2"(CLLocation) and initializing each with "obj1"(id) and "obj2"(id) coordinates
CLLocation *location1=[[CLLocation alloc] initWithLatitude:((Store*)obj1).geoPoint.latitude longitude:((Store*)obj1).geoPoint.longitude];
CLLocation *location2=[[CLLocation alloc] initWithLatitude:((Store*)obj2).geoPoint.latitude longitude:((Store*)obj2).geoPoint.longitude];
//Creating "dist1"(float) and setting its value to the distance between "location1"(CLLocation) and the user's location
float dist1 =[location1 distanceFromLocation:self.locationManager.location];
//Creating "dist2"(float) and setting its value to the distance between "location2"(CLLocation) and the user's location
float dist2 = [location2 distanceFromLocation:self.locationManager.location];
//If the distances are equal - the order will stay the same
if (dist1 == dist2) {
return NSOrderedSame;
}
else if (dist1 < dist2) { //If "dist1"(float) is smaller than "dist2"(float) - "dist2"(float) will be before "dist1" in the array
return NSOrderedAscending;
}
else { //else - "dist2"(float) will be before "dist1" in the array
return NSOrderedDescending;
}
}];
//If "twentyClosestStores"(NSMutableArray) is nil
if (self.twentyClosestStores==nil) {
//Initializing "twentyClosestStores"(NSMutableArray)
self.twentyClosestStores=[NSMutableArray array];
}
//If "previousTwentyStores"(NSMutableArray) is nil
if (self.previousTwentyStores==nil) {
//Initializing "previousTwentyStores"(NSMutableArray)
self.previousTwentyStores=[NSMutableArray array];
}
//Setting "previousTwentyStores"(NSMutableArray) to "twentyClosestStores"(NSMutableArray)
self.previousTwentyStores=self.twentyClosestStores;
//Cleaning (reInitializing) "twentyClosestStores"(NSMutableArray)
self.twentyClosestStores=[NSMutableArray array];
//Adding indexes 0-19 of "allStores"(NSMutableArray) (20 closest stores to the user's current location) to "twentyClosestStores"(NSMutableArray)
for (int i = 0; i < 20; i++) {
[self.twentyClosestStores addObject:[self.allStores objectAtIndex:i]];
}
}
#pragma mark - Start/stop monitoring methods
//For updating monitoring
-(void)locationChangeHandler
{
//If "allStores"(NSMutableArray) isn't nil
if (self.allStores!=nil) {
//Finding the 20 closest stores to he user's location and adding it to "twentyClosestStores"(NSMutableArray)
[self sortClosestStores];
//Stop monitoring "previousTwentyStores"(NSMutableArray) (20 closest stores before user's location updated)
[self stopMonitoringStores];
//Start monitoring "twentyClosestStores"(NSMutableArray)
[self startMonitoringClosestStores];
}
}
//Start monitoring "twentyClosestStores"(NSMutableArray)
-(void)startMonitoringClosestStores
{
//If monitoring isn't availible for "CLCircularRegion"
if (![CLLocationManager isMonitoringAvailableForClass:[CLCircularRegion class]]) {
NSLog(@"Monitoring is not available for CLCircularRegion class");
}
//Run on all "twentyClosestStores"(NSMutableArray)'s objects
for (Store *currentStore in self.twentyClosestStores) {
//Creating "region"(CLCircularRegion) and setting it to "currentStore"(Store)'s circular region
CLCircularRegion *region=[currentStore createCircularRegion];
//Start monitoring "region"(CLCircularRegion)
[self.locationManager startMonitoringForRegion:region];
}
}
//Stop monitoring "previousTwentyStores"(NSMutableArray) (20 closest stores before user's location updated)
-(void)stopMonitoringStores
{
//Run on all "previousTwentyStores"(NSMutableArray)'s objects
for (Store *currentStore in self.previousTwentyStores) {
//Creating "region"(CLCircularRegion) and setting it to "currentStore"(Store)'s circular region
CLCircularRegion *region=[currentStore createCircularRegion];
//Stop monitoring "region"(CLCircularRegion)
[self.locationManager stopMonitoringForRegion:region];
}
}
//Finding a store for region
-(Store*)storeForRegion:(CLCircularRegion*)region
{
//Creating "latitude"(CGFloat) and "longtitude"(CGFloat) and setting it to "region"(CLCircularRegion)'s center.latitude/longtitude
CGFloat latitude=region.center.latitude;
CGFloat longtitude=region.center.longitude;
//Run on all "allStores"(NSMutableArray)'s objects
for (Store *currentStore in self.allStores) {
//If "currentStore"(Store)'s latitude and longtitude is equal to "latitude"(CGFloat) and longtitude(CGFloat)
if (currentStore.geoPoint.latitude==latitude&¤tStore.geoPoint.longitude==longtitude) {
//Returning "currentStore"(Store)
return currentStore;
}
}
//Store not found - returning nil
NSLog(@"No store found for this region: %@",[region description]);
return nil;
}
Store.m:
//Creating and returning a "CLCircularRegion" object of the store
-(CLCircularRegion*)createCircularRegion
{
//Creating "region"(CLCircularRegion) and initializing it with current store information (self) and radios of 200m
CLCircularRegion *region=[[CLCircularRegion alloc] initWithCenter:self.geoPoint radius:200 identifier:self.identifier];
//Setting "region"(CLCircularRegion)'s notifyOnEntry to YES
region.notifyOnEntry=YES;
//Returning "region"(CLCircularRegion)
return region;
}
Note: The delegate methods are being called, even didEnterRegion:
but not always for some reason.
Solved the problem:
(I've decided not to use region monitoring and to do it myself)
-(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations
{
[self checkIfNearStore]; //Not being called in background
}
-(void)checkIfNearStore
{
for (Store *currentStore in self.allStores) {
if ([currentStore.circularRegion containsCoordinate:self.locationManager.location.coordinate]&¤tStore.alreadySendNotification==NO) {
NSLog(@"Entered: %@",[[self storeForRegion:currentStore.circularRegion] address]);
currentStore.alreadySendNotification=YES;
[self.storesAlreadySentNotifications addObject:currentStore];
}
}
for (Store *currentStore in self.storesAlreadySentNotifications) {
if (![currentStore.circularRegion containsCoordinate:self.locationManager.location.coordinate]) {
currentStore.alreadySendNotification=NO;
}
}
}
Thank you very much!
Please review the discussion on the startMonitoringForRegion method below and note that Apple indicates that a value for the region up to 400 works best on later devices. They also indicate that one can expect responses within 3 to 5 mins on average. I am not sure how you are testing the entry/exit of the region but it may be that you are not allowing sufficient time to pass.
Additionally, I suggest you setup break points to examine the monitoredRegions property of the CLLocationManager at strategic points in your code (when you expect the regions to update). You might consider emptying the regions array (see the Namely: header below) before repopulating them with your custom code. I'd inspect that area very carefully to be certain that the expected behavior is occurring.
There are a number of filters that tell the CLLocationManager when a significant change in position has occurred. You should set these values and inspect to see whether they have an impact on the frequency of the updates. Please also note the other properties and methods that I am showing including the desiredAccuracy and the methods for initiating significant updates. Read the discussion for each of these to see whether they apply in your use case. Try to apply changes where needed to force the device to update when it is appropriate in your case.
You might want to implement periodic hit-testing of the specific CLCircularRegion using the user location. This should allow you to further refine location updates at predetermined times/locations in your code. For example you might want to refine the user updates using accuracy and distance filters and then in the delegate method call a query against the closest region(s) in your array. The net effect would be to force a region status check whenever the device moves significantly from the last known position.
I recommend you give a close re-read of the discussion related to the startMonitoringSignificantLocationChanges method (You must use this method to resolve the issue). Apple details the steps that you need to get the updates when the app is in the background. Basically you will have to intercept a key in your app delegate method. Fortunately this SO question has sample code. But before you jump to the code, please review the discussion as recommended since it will give you a much clearer idea of how to master this behavior.
Like many questions. This one requires a number of partial answers to resolve the functionality you need. First make sure you have checked the notes provided above, then try some of this links for partial answers to specific required tasks.
How can I disable any CLRegion objects registered with -startMonitoringForRegion?
Useful responses on how to refine geofencing
A simplified tutorial of geofencing
(Did you use requestAlwaysAuthorization() on the location manager?)
I recommend that you read the discussions of the methods and properties to see hints from Apple. You should also feel free to cmd-click on the methods and properties in code editor and review the comments in the header files. These will provide more insight from Apple that is not available in the documentation or Quick Help.
As a quick example the following header comments seem relevant to your objective. You might want to implement and monitor the delegate method and the requestStateForRegion method mentioned in the comment header:
/*
* locationManager:didDetermineState:forRegion:
*
* Discussion:
* Invoked when there's a state transition for a monitored region or in response to a request for state via a
* a call to requestStateForRegion:.
*/
@available(iOS 7.0, *)
optional public func locationManager(manager: CLLocationManager, didDetermineState state: CLRegionState, forRegion region: CLRegion)