We have previously looked at how to use region monitoring with iBeacons using a CLBeaconRegion. Today we’ll look at the CLCircularRegion for region monitoring. There are some similarities between the CLBeaconRegion and CLCircularRegion. Both inherit from CLRegion which means there are some aspects that they share. In our iBeacons tutorial we created region monitoring which looks out for bluetooth transmitters called iBeacons. When the app detects the iBeacon it triggers a delegate method to be called which allows you to inspect a major and minor value to know which region was triggered. You can program the app to detect when you come in to range, or when you leave range.
With the CLCircularRegion we use geo location instead of bluetooth signals. This allows us to create geo fences which are defined by a centre point on a map along with a radius. When the phone passes in or out of the CLCircularRegion, the appropriate delegate method is fired and your app can be programmed to act accordingly. Common uses include controlling a smart home and setting a geo fence which when you leave the fence, it switches off your heating. Popular todo task manager apps, such as OmniFocus, use geo fences to let you know when you enter a region on a map where you need to perform a task.
In todays tutorial we’ll look at how to create a CLCircularRegion and how to start monitoring for that region.
Lets begin by creating a single view application.
In ViewController.swift, import CoreLocation:
import CoreLocation
Next, create a location manager just below the class declaration:
let locationManager = CLLocationManager()
In viewDidLoad we need add the following:
locationManager.delegate = self locationManager.requestAlwaysAuthorization()
Line 1 we set the location managers delegate to self. The reason for this is that ViewController will be the class that is informed whenever a region change is detected. We also need to use it to trigger off the events when the user authorises the app. Note that you will get an error at this point because we haven’t told ViewController.swift that it conforms to the CLLocationManagerDelegate yet. Ignore this error for now. We’ll fix it in a moment.
Line 2 is where we request authorisation from the user.
The CoreLocation documentation tells us that we need to add some keys to the info.plist file. The keys we need to add are:
NSLocationWhenInUseUsageDescription
NSLocationAlwaysAndWhenInUseUsageDescription
Add a row for each key in info.plist, and add each key, and provide a string describing why you want to access the users location. It doesn’t matter what string you use for this test, but if you are using location for a live app, you will need to write what you are using location for in the most succinct way possible.
Implementing Delegate Methods
In viewDidLoad we set the delegate to self. We need to make sure we declare that this class conforms to the CLLocationManagerDelegate class. We do that by modifying our class declaration to specify that class as follows:
class ViewController: UIViewController, CLLocationManagerDelegate {
We now need to implement 3 delegate methods. These are:
locationManager:didEnterRegion:
locationManager:didExitRegion:
locationManager:didChangeAuthorizationStatus:
The easiest way to implement these is to let Xcode do most of the typing for you. Start typing location and code sense will show location related methods. Code sense is smart enough that you do not need to type in a whole word. One way I found that helps things move quicker is to start typing parts of a word. Typing just len is enough to bring up the delegate method you want to use. The “l” brings up some location manager delegates, and “en” refines it to the didEnter… as seen below:
Hit enter when the correct methods are selected, and code sense will fill them in for you. When you have repeated this for each of the 3 delegate methods you will have the following:
func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) { } func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) { } func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { }
Each time a region entry is detected, the method on line 1 will be called.
Each time a region exit is detected, the method on line 5 will be called.
When you ask the user to give permission to use location, the method on line 9 will be called. We can use this method to start monitoring for a region.
Implement the following:
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { print("Authorized status changed") if CLLocationManager.authorizationStatus() == CLAuthorizationStatus.authorizedAlways || CLLocationManager.authorizationStatus() == CLAuthorizationStatus.authorizedWhenInUse { let circularRegion = CLCircularRegion.init(center: (locationManager.location?.coordinate)!, radius: 200.0, identifier: "Home") circularRegion.notifyOnEntry = true circularRegion.notifyOnExit = true locationManager.startMonitoring(for: circularRegion) } }
The reason we are putting this code in this delegate method is because the locationManager.requestAlwaysAuthorization() we implemented earlier runs asynchronously, and if we create a region and start monitoring in viewDidLoad, the user is unlikely to be able to respond to the question before region monitoring starts, and thus, the app won’t be authorised, and it will either crash, or not run the code needed.
Line 2 we print to the console just to let us know that this delegate method was triggered.
Line 3 we check if the user granted permission to use location services. We are checking here if they allowed “always” or “when in use”. Either is fine for the demonstration, but it will work better if “always” is selected so that you can see how region monitoring works while your app is in the background.
Line 4 we are creating a CLCircularRegion and basing it on the phones current coordinates. We are setting a radius of 200 meters, and giving it an identifier of “Home”.
Just a note to add here: Region entries and exits are not always immediate. The reason for this is that Apple wants to be sure that the user did actually enter or exit a region. Certain threshold conditions need to be met by the device for it to be determined an exit or entry. If your radius is set to 200 meters, Apple will wait for the 200M boundary to be passed, and then add some extra to that in case you turn around and come back in. You also need to be out of region for 20 seconds for an entry to be triggered again.
Also, the reason for choosing 200 meters is that Apple indicates that we can use this as a guideline:
The specific threshold distances are determined by the hardware and the location technologies that are currently available. For example, if Wi-Fi is disabled, region monitoring is significantly less accurate. However, for testing purposes, you can assume that the minimum distance is approximately 200 meters.
In lines 7 and 8 we are setting the circularRegion to notify us when it detects and entry or exit from the region.
On line 9, we start monitoring for this region. The documentation informs us that each app can monitor up to 20 regions at a time. That could be 20 CLCircularRegions, 20 CLBeaconRegions, or a mixture of both. It is also possible to cascade regions so that more than 20 can be monitored, although this “cheating way” is out of the scope of this tutorial. Also note that if you recreate a region and start monitoring for it, and it has the same identifier as another region, it will overwrite that region. Regions are based on identifier in this regard.
With monitoring started, the phone is now monitoring for region changes. If your phone leaves “Home” then didExit will trigger, and if your phone comes back in to the “Home” region, the didEnter will trigger.
You can stop monitoring a region by calling the stopMonitoring: method and passing in the region you do not want to monitor. If you have discarded the CLCircularRegion at this point, just create a new one with some random coordinates, but the correct identifier. The important part is that the region you want to stop has the correct identifier provided.
Adding Notifications
Because of this app requiring you move around in and out of regions, logging to the console on each entry or exit simply won’t do (unless you carry your MacBook around with your iPhone tethered to it). We will create a notification that triggers on an exit and entry so that you can see when you enter or exit a region by way of a push notification on screen. We have set the region to have a 200 meter radius. Please do not measure this out precisely. Many factors determine the accuracy of this. If you are in a busy WiFi area, accuracy will be increased, so I suggest walking around 250 meters or perhaps 300 meters, waiting for 30 seconds or so, and then walking back in to range. You should see a notification to say you left, and one to say you entered.
Lets implement this. There are a few steps to getting local notifications to work. The first is to request authorisation from the user. We will do this in AppDelegate.swift. The code used for this is provided in the Apple documentation:
First, import UserNotifications:
import UserNotifications
Next, add the code as detailed in the documentation to didFinishLaunchingWithOptions (in AppDelegate.swift):
let center = UNUserNotificationCenter.current() // Request permission to display alerts and play sounds. center.requestAuthorization(options: [.alert, .sound]) { (granted, error) in // Enable or disable features based on authorization. }
We don’t need to do anything on line 4 here. We will focus on what this can be used for in another tutorial.
We are requesting sounds and alerts permissions. Other options are available, but we will look at the the other options in another tutorial.
Back in ViewController.swift, make sure you also import UserNotifications:
import UIKit import CoreLocation import UserNotifications
Add a new method as follows:
func fireNotification(notificationText: String, didEnter: Bool) { let notificationCenter = UNUserNotificationCenter.current() notificationCenter.getNotificationSettings { (settings) in if settings.alertSetting == .enabled { let content = UNMutableNotificationContent() content.title = didEnter ? "Entered Region" : "Exited Region" content.body = notificationText content.sound = UNNotificationSound.default() let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) let request = UNNotificationRequest(identifier: "Test", content: content, trigger: trigger) notificationCenter.add(request, withCompletionHandler: { (error) in if error != nil { // Handle the error } }) } } }
Line 1: We are creating a function that has the responsibility of forming a notification and scheduling it. We accept a string of text for the notification and a bool to indicate if this is an entry or not.
Line 2 we create our notification centre.
Line 4 we get the settings which has a handler. This is a better way so that we can create the correct type of notification. We are basically using the handler here (on line 5) to check if the alertSetting is enabled. It will be if you accepted notifications when you loaded up the app.
Line 6 we begin creating the content of the notification.
Line 7 we check if it was an entry or an exit and then set the content.title to either Entered Region or Exited Region.
Line 8 we set the body of the notification to the text we will pass in.
Line 9 we set the sound to be default.
Line 11 we create a trigger. This is “when” the notification will fire. The most simple one to use in this example is the UNTimeIntervalNotificationTrigger. There are 4 types of trigger we can use, all of which conform to the UNNotificationTrigger class. The other are UNCalendarNotificationTrigger (useful for specifying a specific time and date), a UNLocationNotificationTrigger (used also for location based notifications if using region monitoring isn’t for you), and UNPushNotificationTrigger which is when a notification is triggered from outside of the app (such as when an email arrives on the server, a push notification can be triggered to get the app to respond accordingly).
So we are using the most simple Time Interval Notification and telling it to fire a notification in 1 second from now with no repeat.
Line 13 we create a request. I just threw in the identifier as “Test”, but if you need to track notifications and remove them, you’ll need something programmed in here. We also pass in the content and the trigger at this point.
Line 15 is where we add the notification request. It has a completion handler which can be used to detect if an error occurred.
Using Notifications
So we just created a function that accepts some text and a bool so that a notification can be put on the lock screen. We now need to make a call to this from the relevant place.
func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) { print("Did Arrive: \(region.identifier)") fireNotification(notificationText: "Did Arrive: \(region.identifier) region.", didEnter: true) } func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) { print("Did Leave: \(region.identifier)") fireNotification(notificationText: "Did Exit: \(region.identifier) region", didEnter: false) }
In the first delegate method (didEnterRegion) we are printing to the console on line 2. On line 3 we call the fireNotification method and pass it a string, as well as set didEnter to true).
We do similar on line 8 on the didExitRegion delegate method, but just change the wording slightly, and also set didEnter to false.
Testing the App
To test the app you need to walk around, at least 200 meters from your current location where you first launched the app, and then wait a few moments for an exit notification to fire. Apple tell us it can take a few minutes for this to happen (worse case). When you receive that notification, wait around 30 seconds longer, and then walk back in to the region. You should get an entry alert. In my testing entries tend to be quicker than exits for notifications. Circumstances around you, including wifi coverage in the area, proximity to cell towers, and other factors can change performance.
In my test results I walked 304M from my home (measured after on Google Maps), which is where I was notified of my exit. The exit would have occurred at around 200M, but Apple may have taken some time to notify me as it needed to be sure I wasn’t on the fringe and about to walk back in.
Entry notified me just over 200 meters from my home.
As you can see from the results, geofences are not 100% accurate in regards to radius. If geofences are separated by some distance such as your home, your office, your parents, a local church, then approaching any of these will trigger a notification, but not at exactly the specified radius due to the many factors involved in working out where the edge of the fence is, mixed with trying to balance battery life.
You can download the full app from here.
Leave a Reply
You must be logged in to post a comment.