When iOS 5 launched in October 2011, one of the features that caught my attention was location based reminders. Location based reminders provided the ability to connect a specific reminder to a specific location. An example could be, written in English, “When I arrive at the supermarket, remind me to buy flowers for my wife” or “when I leave work, remind me to call Fred”. When you approach a supermarket a reminder pops up telling you to go get flowers or when I leave the office, I get a reminder to call someone on my way home. Although GPS/location features have been available for most generations of the iPhone, mixing that power with context can make your app very powerful.
Of course, geofencing isn’t only for the built in reminders app that shipped with iOS 5. Apple has opened it up for developers to access. Today, we’ll be looking at how to work with geofencing which specifically uses the CLRegion Class as well as the CLLocationManager. A good place to start, as always, is the class reference. If you are unsure how to understand class references, methods, passing messages, properties etc… then take a read of this post and return back when done.
The CLRegion Class and CLLocationManager Class References
As with writing any code for an app, you regularly need to look at the documentation to see exactly what you need to do. Using the class reference should become second nature for you. I personally have it open on a second screen so I can keep the main screen open for Xcode.
CLRegion Class
The CLRegion Class is relatively small in comparison to others such as NSString. By that, I mean it simple has 2 instance methods and 3 properties. In the overview we learn that the “CLRegion class defines a geographical area that can be tracked…”. We then learn that we need to register an instance of this class with a CLLLocationManager object. We are finally instructed to use the -startMonitoringForRegion:desiredAccuracy: method in CLLocationManager to kick it in to action.
When we first create an instance of CLRegion, we need to initialise it with a circular region. The method for this is initCircularRegionWithCenter:radius:identifier:. Looking at the description of this method we see the more details method which is – (id)initCircularRegionWithCenter:(CLLocationCoordinate2D)center radius:(CLLocationDistance)radius identifier:(NSString *)identifier.
Breaking that method down we see we need to specify a centre of the region… in this case it looks for a CLLocationCoordinate2D structure which by clicking on the name, we see requires latitude and longitude in degrees. We next need a radius which is a typedef of a CLLocationDistance which is a double. Finally it requires an identifier which is an NSString. The identifier is just a unique string that lets you associate this region with an object.
The other method which we might want to use is – (BOOL)containsCoordinate:(CLLocationCoordinate2D)coordinate. This is used as a simple hit-test to see if a specified region contains a particular coordinate.
Note that all methods here have been available since iOS 4 although since the introduction of iOS 5, we can now monitor the regions and wake up the app to do something when you either leave or enter a particular location.
One last thing to note with the Organiser documentation (the documentation that comes as part of Xcode) is that the startMonitoringForRegion:desiredAccuracy: it recommends you use is now deprecated since iOS 6. It can still be used, but you should look at using the newer alternative where possible which is listed in the documentation as startMonitoringForRegion:. If you need to make the app compatible with both iOS 5 and 6, this tutorial might give you some ideas of how to work with both deprecated and their replacements at the same time.
CLLocation Manager Class
Looking at the CLLocationManager class reference, we need to create a CLLocationManager object. I recommend you use the 5 steps provided by Apple which are:
1. Always check to see whether the desired services are available before starting any services and abandon the operation if they are not.
2. Create an instance of the CLLocationManager class.
3. Assign a custom object to the delegate property. This object must conform to the CLLocationManagerDelegate protocol.
4. Configure any additional properties relevant to the desired service.
5. Call the appropriate start method to begin the delivery of events.
Note that we need to use the CLLocationManagerDelegate here as well. We’ll cover that when we jump in to the tutorial and create our own location based app.
Assuming we are using iOS 6 then it is recommended that we use startMonitoringForRegion:. The full method is called – (void)startMonitoringForRegion:(CLRegion *)region and we see we need to specify a region that we are monitoring. A few points to note are that this method needs to be called for every region you want to monitor. If you have 5 regions to monitor in your app, then this needs to be called 5 times… once for each region. If you call this method twice for the same region, the new region replaces the old region.
We also learn in this method discussion that you can check what regions are being monitored by looking at the monitoredRegions property.
When a region event occurs, which would be entering or leaving a region, the CLLocationManagerDelegate is notified through the locationManager:didEnterRegion: and locationManager:didExitRegion: methods.
Another important bit of information we learn is that iOS allows for up to 20 regions to be monitored per app and that on iOS 6, if you use an iPhone 4S then you get best results from 1 to 400 meters. If you are using iOS 5 on an iPhone 4S or later, the radius limit is lower at a max of 150 meters.
Now that we have a summary of what we need to use, lets get creating a simple app as an example of one way to accomplish a task. For this test, I want to create a simple application that you set the location of your office. You will be notified when you arrive at work and notified when you leave work. This app is of course pointless because you know exactly when you arrive at work and when you leave. But, feel free to use this as a foundation to a project where you can feed different locations in to be monitored and where you can do other things with the app when you enter and exit a region.
The Geofence Project
As always, create a new Single View Application to start with (click the link for instructions on how to do that).
Set the app us as pictured below. We are using the standard View Controller set up when we created a Single View app. I dragged a map in to it, added a toolbar and added a couple of buttons. The first button is Update Location which is used to zoom in to the current location. The second is Monitor which takes the coordinates of the centre of the viewable map and will use them to define a region.
Because we are using a Single View Application from the start, the initial view controller already has an associated class called ViewController.h/m.
Customising the Map
Our next task is to customise the map. Select the map and then click on the Attributes Inspector. The only change I made here was to show the users current location. We could have done this through code, but to save time we can just check the box. Next, I CTRL+dragged from the map to my ViewController.h file and created a @property for the map. This will allow us to use the map from within the code later on in the tutorial. When doing this, you’ll get an error. To get rid of the error, include this line of code just below the UIKit import.
#import
This line lets us use the MapKit which allows us to work with the map.
This is probably a good time to get you to import the MapKit framework and CoreLocation framework in to your app.
To see our progress, which naturally isn’t much yet, load up the app. Because we checked the Shows User Location box, the app will naturally ask for permission to use location services the first time it is run. We won’t go in to the error checking in this tutorial, so for all intents and purposes, we can assume that you have an iOS device with location services or are using the simulator, both of which will allow you to agree to using the services. We can also assume that the services will be available to use. When putting an app in to production, you need to include all the error checking because the user might block access to the location services or there could be another problem and if no error checking is in place, your app could crash and will also not function as intended.
Zooming in to Our Current Location
To make the map more useful, lets automatically zoom in to our current location to a radius of about 20 miles which should cover many peoples commutes to a place of work. As with most tutorials here, we’ll go for the basics so you can see the app working with code. You can then tweak as needed to suit your own needs.
To do this, the code in ViewController needs to be as follows:
#import
#import
#import
@interface ViewController : UIViewController
@property (weak, nonatomic) IBOutlet MKMapView *mapView;
@property (strong, nonatomic) CLLocationManager *locationManager;
@end
We need to import CoreLocation (seen on line 3).
Line 5 adds the CLLocationManagerDelegate as this is needed to start the location services.
On line 8 we add a locationManager property from CLLocationManager. This is used to set up the location services we need to move forward with this app.
Moving to the implementation of ViewController, we need to initialise the locationManager and set the accuracy and the delegate. We do this with the following 4 lines of code which are inserted right below the [super viewDidLoad]; line in the viewDidLoad method.
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters;
self.locationManager.delegate = self;
[self.locationManager startUpdatingLocation];
Line 1 alloc and inits the locationManager. We use self.locationManager here as this is a property found within this custom class.
Line 2 we set the desired accuracy. There are several options here which include best for navigation up to three kilometers. I opted for HundredMeters in this instance although you can set this or give this user an option to set it to suit their needs.
Line 3 we set the delegate for locationManager to self as this class will be the one receiving the updates.
On line 4, we send the locationManager an update to startUpdatingLocation. This allows us to receive the devices current location and then use it to zoom in on.
The Update Location button is next configured to run the method below. The updateLocation method zooms in to your current location. If you move or the device hasn’t yet got a location for you, click it again a few seconds later and it should eventually zoom to your current location.
- (IBAction)updateLocation:(UIBarButtonItem *)sender {
float spanX = 0.58;
float spanY = 0.58;
self.location = self.locationManager.location;
NSLog(@"%@", self.location.description); //A quick NSLog to show us that location data is being received.
MKCoordinateRegion region;
region.center.latitude = self.locationManager.location.coordinate.latitude;
region.center.longitude = self.locationManager.location.coordinate.longitude;
region.span = MKCoordinateSpanMake(spanX, spanY);
[self.mapView setRegion:region animated:YES];
}
The code above has three main parts. The two float variables (spanX and spanY) are used to set the span of the map. I think a 20 mile radius is good here. According to the documentation 1 degree is equal to 69 miles. With a bit of maths I calculated that a 40 mile span was 0.58 degrees hence the 0.58 spanX and spanY.
Line 4 is used to get the current location from self.locationManager.location which is then stored in @property (strong, nonatomic) CLLocation *location; which we put in the header.
Lines 6 to 10 are where we define a region that we want to zoom in to. We set the region.centres lat and lng coordinates, create a span and then call the setRegion:region method on self.mapView to zoom in.
Now that we have the basics in place, lets move on to the real purpose of the app which is to add a CLRegion and monitor entrances and exits from the region.
Setting Up the CLLocationManager, CLLocationManagerDelegate and the CLRegion
We have already set up CLLocationManager and CLLocationManagerDelegate and enabled location services on the device. What we now need to do is create a CLRegion and pass that to the CLLocationManager and start monitoring for regions. We do this with the following code.
I will first mention that the Monitor button on the toolbar will be able to be pressed and grab the centre of where the map currently is. When this method is called, we will also set the radius of 20 meters and provide an identifier NSString called Work.
- (IBAction)monitorThisRegion:(UIBarButtonItem *)sender {
CLLocationCoordinate2D centre;
centre.latitude = self.mapView.centerCoordinate.latitude;
centre.longitude = self.mapView.centerCoordinate.longitude;
[self.locationManager startMonitoringForRegion:[[CLRegion alloc] initCircularRegionWithCenter:centre radius:20.0 identifier:@"Work"]];
}
Line 1 is the IBAction meaning that when the Monitor button is pressed, this method is called.
Lines 2 – 4 we get the centre coordinate of the visible map. You can scroll around and zoom to where your office is, put it in the centre of the map and tap Monitor. These lines of code will get the coordinates at the centre of the screen.
Line 5 we startMonitoringForRegion and pass in an alloc/init of a CLRegion as an argument rather than creating a property. You could put a property in the header and alloc init prior to passing it as the argument to startMonitoringForRegion, but in this case I decided to cut out 2 lines of code and just alloc/init when passing. We don’t need to access this information and will only alloc/init again if we monitor for a new region.
Making Use of the CLRegion
Now that we have started the monitoring services we now need to sort out what we do when we enter or exit the region. To do this, we need to turn to the CLLocationManagerDelegate and make use of the locationManager:didEnterRegion: and locationManager:didExitRegion:. Each time we move in or out of a monitored region or within the 20 meters (in this case) of the region, we will trigger one of these events.
The code for this is very simple. We just need to add the delegate methods as below:
-(void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region {
NSLog(@"Welcome to %@", region.identifier);
}
-(void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region {
NSLog(@"Bye bye");
}
-(void)locationManager:(CLLocationManager *)manager didStartMonitoringForRegion:(CLRegion *)region {
NSLog(@"Now monitoring for %@", region.identifier);
}
The first method is called when we enter a specified region. In this example I simply used NSLog put a Welcome to %@ which pulls the name of the region in.
The second method does exactly the same other than I just NSLog a Bye Bye.
The final method lets us know when region monitoring has started. This method doesn’t start the region monitoring, it simply is called when we start the region monitoring which is done in the monitorThisRegion custom method above in the code.
Making the App More Useful
In its current form, the app isn’t useful at all. It only does something useful on the simulator which can NSLog to the screen to let you know if you entered or exited a location.
To make this a little more useful we will have the enterRegion and exitRegion set an NSUserDefault and we’ll have a label which will let you know if you are currently in or out of a location… ie, you are at work or you are not at work. Kind of lame, I know, but modify and make something useful with the app!
I recently wrote an NSUserDefaults tutorial so figured I might as well just link to that to provide some background. What we will do here is have just a simple field that lets us know if we are at work, not at work or unknown.
Lets start by creating a label and connecting an outlet up for it.
Next we need to add some code for NSUserDefaults as follows:
-(void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region {
NSUserDefaults *standardDefaults = [NSUserDefaults standardUserDefaults];
[standardDefaults setObject:@"Yes" forKey:@"atWork"];
self.workLabel.text = @"You are at Work";
[standardDefaults synchronize];
}
-(void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region {
NSUserDefaults *standardDefaults = [NSUserDefaults standardUserDefaults];
[standardDefaults setObject:@"No" forKey:@"atWork"];
self.workLabel.text = @"You are not at Work";
[standardDefaults synchronize];
}
The didEnterRegion method creates the standardDefaults object. On line 3 we create a key called atWork and set the object to Yes. We then set the label to say, you are at work. We then syncronise just to be on the safe side.
The didExitRegion is almost exactly the same other than it sets the key to No for being atWork and sets the label accordingly.
Testing
You can test the app through either the simulator or on a device. When using the simulator, you will need to set the location to Apple and mark the region to monitor. You then need to go to a custom set of coordinates. I selected -34, 138 which is a park somewhere in the world. When flicking between those two coordinates your app will tell you if you are at work or not at work. Because region monitoring only detects when you enter or leave a region, you would need to run more code that handles situations when you define your current region as work. The reason for this is because the first time the code checks, you have neither entered or exited the current region and therefore, the device doesn’t tell you if you are at work or not.
On an actual device, go to your current location and scroll the map a little to somewhere 30 – 50 meters away and set that as a region to monitor. Close the app out and walk to that region. When you open up the app you should have a message telling you that you are at work. Likewise, if you close it out and walk away, the next time you open up the app it will tell you that you are not at work.
Conclusion
As mentioned earlier, this code on its own isn’t very helpful. Look at it more as a basic template that you need to fill in the gaps to make it work. The emphasis I try to make is that you can customise the two delegate methods to do more interesting things. As of iOS 5 and later, the enter and exit region methods are called and your app launched in the background. You can get creative with that by adding push notifications or by performing another action.
Please post any comments or observations in the comments below. Remember that I am still learning, so appreciate constructive feedback!
Header Code
#import
#import
#import
@interface ViewController : UIViewController
@property (weak, nonatomic) IBOutlet MKMapView *mapView;
@property (strong, nonatomic) CLLocationManager *locationManager;
@property (strong, nonatomic) CLLocation *location;
@property (weak, nonatomic) IBOutlet UILabel *workLabel;
@end
Implementation Code
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
NSUserDefaults *standardDefaults = [NSUserDefaults standardUserDefaults];
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters;
self.locationManager.delegate = self;
[self.locationManager startUpdatingLocation];
if ([[standardDefaults stringForKey:@"atWork"] isEqualToString:@"Yes"]) {
self.workLabel.text = @"You are at Work";
} else if ([[standardDefaults stringForKey:@"atWork"] isEqualToString:@"No"]) {
self.workLabel.text = @"You are not at Work";
} else {
self.workLabel.text = @"You are somewhere";
}
}
- (IBAction)updateLocation:(UIBarButtonItem *)sender {
float spanX = 0.58;
float spanY = 0.58;
self.location = self.locationManager.location;
//NSLog(@"%@", self.location.description); //A quick NSLog to show us that location data is being received.
MKCoordinateRegion region;
region.center.latitude = self.locationManager.location.coordinate.latitude;
region.center.longitude = self.locationManager.location.coordinate.longitude;
region.span = MKCoordinateSpanMake(spanX, spanY);
[self.mapView setRegion:region animated:YES];
}
- (IBAction)monitorThisRegion:(UIBarButtonItem *)sender {
CLLocationCoordinate2D centre;
centre.latitude = self.mapView.centerCoordinate.latitude;
centre.longitude = self.mapView.centerCoordinate.longitude;
[self.locationManager startMonitoringForRegion:[[CLRegion alloc] initCircularRegionWithCenter:centre radius:20.0 identifier:@"Work"]];
}
-(void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region {
NSUserDefaults *standardDefaults = [NSUserDefaults standardUserDefaults];
[standardDefaults setObject:@"Yes" forKey:@"atWork"];
self.workLabel.text = @"You are at Work";
[standardDefaults synchronize];
}
-(void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region {
NSUserDefaults *standardDefaults = [NSUserDefaults standardUserDefaults];
[standardDefaults setObject:@"No" forKey:@"atWork"];
self.workLabel.text = @"You are not at Work";
[standardDefaults synchronize];
}
-(void)locationManager:(CLLocationManager *)manager didStartMonitoringForRegion:(CLRegion *)region {
NSLog(@"Now monitoring for %@", region.identifier);
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
Lo says
Could you please let me know how to test this code in simulator?
Lo says
Hi,
I am not able to monitor the region didEnter n didExit…. Not working… Please help!!
Tulakshana Asanka says
initCircularRegionWithCenter is deprecated as of iOS 7.0, macOS 10.10 and watchOS 2.0