Search code examples
iosobjective-ccocoa-touchcllocationmanagernsset

CLLocationManager monitoredRegions (NSSet) is not correct, or maybe something else?


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&&currentStore.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]&&currentStore.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!


Solution

  • Region Radius

    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.

    region-discussion

    Region Monitoring

    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.

    region-monitoring

    Filters

    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.

    filters

    Hit Testing User Location

    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.

    clcircular-region-hit-testing

    Background Updates for Location Changes

    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.

    significantlocationchanges

    Partial Answers

    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?

    Namely: stop-monitoring

    Useful responses on how to refine geofencing

    A simplified tutorial of geofencing

    (Did you use requestAlwaysAuthorization() on the location manager?)

    Nota Bene

    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)