iOS Programming · · 8 min read

Building a Geo Targeting iOS App in Swift

Building a Geo Targeting iOS App in Swift

Geo targeting is the method to show different content to your users based on their geographic location, such as country, region, city or any other criteria. There are a lot of common uses of geo targeting. Imagine a customer visiting your competitor’s restaurant. We can show him or her special offer in your restaurant and engage customer’s attention and return him into your restaurant back. If user visited a lot of car dealerships in last few days, it’s likely that he want to find new car. So, we can show him our car advertising. This targeted advertising will be more successful than other one which we show to random user.

In this tutorial I will show you how to implement geo targeting functionality on iOS. I will describe Apple’s standard method with CLRegion class. Also, I will show you how to test this uncommon feature. We will see how to implement complex tracking logic. Finally, I will describe you how to create your custom regions and explain you why and when custom regions are better than CLRegion. You can use geo targeting to develop a lot of innovative location-based mobile apps.

geotarget-demo

Geo Targeting Project

Let’s assume that we want to track our user visiting restaurant. I want to start with project setup. I created new Swift Single View Appliction project with name GeoTargeting.

Go to Main.storyboard and add new Map View to a ViewController’s view. Create @IBOutlet for this Map View in ViewController.swift. Compiler will show you an error, but wait a minute, we will fix it. Our storyboard is ready. Now we can move to ViewController.swift.

Let’s import Apple’s frameworks MapKit and CoreLocation. And add their protocols to the ViewController. Now ViewController.swift should look like this:

import UIKit
import MapKit
import CoreLocation

class ViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
    
    @IBOutlet weak var mapView: MKMapView!
    
    override func viewDidLoad() {
        super.viewDidLoad( )
    }
}

Now we can setup our mapView and create new locationManager.

// 1. create locationManager
let locationManager = CLLocationManager()

override func viewDidLoad() {
    super.viewDidLoad( )

    // 2. setup locationManager
    locationManager.delegate = self;
    locationManager.distanceFilter = kCLLocationAccuracyNearestTenMeters;
    locationManager.desiredAccuracy = kCLLocationAccuracyBest;

    // 3. setup mapView
    mapView.delegate = self
    mapView.showsUserLocation = true
    mapView.userTrackingMode = .Follow

    // 4. setup test data
    setupData()
}

What we add to the ViewController:

  1. Create an instance of locationManager, which will detect user’s location changes.
  2. Setup locationManager. We set manager’s delegate to the ViewController for track user’s location and monitor regions within ViewController. Also, we configure location tracking for best accuracy.
  3. Setup mapView. We set mapView’s delegate for additional drawing on mapView. Then we ask mapView for showing user’s location to us and to follow this location. This will help us to understand better if user in a region or not. You also can setup MapView’s delegate and showsUserLocation via Storybord. We did it within the code for better visualization.
  4. Setup test data. We will add this method later.

Now it’s time to start tracking user’s location. But we don’t know if we have correct access rights. Let’s check it.

override func viewDidAppear(animated: Bool) {
    super.viewDidAppear(animated)

    // 1. status is not determined
    if CLLocationManager.authorizationStatus() == .NotDetermined {
        locationManager.requestAlwaysAuthorization()
    }
    // 2. authorization were denied
    else if CLLocationManager.authorizationStatus() == .Denied {
        showAlert("Location services were previously denied. Please enable location services for this app in Settings.")
    }
    // 3. we do have authorization
    else if CLLocationManager.authorizationStatus() == .AuthorizedAlways {
        locationManager.startUpdatingLocation()
    }
}

Let’s elaborate this method. First of all, we check authorization status in viewDidAppear method because we can recheck status after user changed it in settings and come back to our app.
So, which

  1. If status is not determined, then we should ask for authorization. We ask for “Always” permission because this is the only status which works with CLRegion monitoring.
  2. If authorization has been denied previously, we should inform the user that our app will work better if he will allow location services. We used method showAlert(title: String) here. This is shortcut method for displaying information UIAlertController with “Cancel” button. We will use this method few times more later.
  3. If we have authorization we can start the standard location service updating.

Now you can run app. You may ask me – why app didn’t prompt location authorization alert? We should go to ...-Info.plist file and add new row NSLocationAlwaysUsageDescription and add value for this key, e.g. “Regions needs to always be able to access your location.”. This key is required when we use requestAlwaysAuthorization() method. Otherwise system will ignore location request. This key describes why your system want to use user’s location.

After adding this key you can run app once again. Now system will prompt authorization alert with our description.

Of course, there are few more statuses of the location authorization, that our app may have. But this is basic which we need for this tutorial.

Now we are ready to monitor regions, let’s add them.

func setupData() {
    // 1. check if system can monitor regions
    if CLLocationManager.isMonitoringAvailableForClass(CLCircularRegion.self) {

        // 2. region data
        let title = "Lorrenzillo's"
        let coordinate = CLLocationCoordinate2DMake(37.703026, -121.759735)
        let regionRadius = 300.0

        // 3. setup region
        let region = CLCircularRegion(center: CLLocationCoordinate2D(latitude: coordinate.latitude,
            longitude: coordinate.longitude), radius: regionRadius, identifier: title)
        locationManager.startMonitoringForRegion(region)

        // 4. setup annotation
        let restaurantAnnotation = MKPointAnnotation()
        restaurantAnnotation.coordinate = coordinate;
        restaurantAnnotation.title = "\(title)";
        mapView.addAnnotation(restaurantAnnotation)

        // 5. setup circle
        let circle = MKCircle(centerCoordinate: coordinate, radius: regionRadius)
        mapView.addOverlay(circle)
    }
    else {
        print("System can't track regions")
    }
}

// 6. draw circle
func mapView(mapView: MKMapView, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer {
    let circleRenderer = MKCircleRenderer(overlay: overlay)
    circleRenderer.strokeColor = UIColor.redColor()
    circleRenderer.lineWidth = 1.0
    return circleRenderer
}

Let’s understand together what we did in setupData() method step by step.

  1. You always should check if region monitoring is supported on the user’s device. isMonitoringAvailableForClass will be false if user denied location request, user disabled Background App Refresh or if device is in Airplane mode.
  2. Here we created test restaurant for our tutorial. This is ok in our case, but in real project you should create separate class for this object.
  3. Finally, we created region, which app will monitor. We use restaurant’s title as an identifier for the region. We need it, because this is the only way to detect which region user visited. So, you don’t need to store strong ref to CLRegion, but you might store region’s identifier for work with it in the future.
  4. For better visualization we added annotation in the center of our region.
  5. For better visualization we added circle on the map, which represent region’s boundaries.
  6. It is a MKMapViewDelegate’s method for drawing our circle.

That’s it with project setup. We are ready for region monitoring.

Apple’s CLRegion

In this tutorial we are working with geographical regions. Apple’s CLRegion – is an circle area of a specified radius around a known location. So, with CLRegion you can monitor only circle areas. Let’s add some code for tracking regions.

// 1. user enter region
func locationManager(manager: CLLocationManager, didEnterRegion region: CLRegion) {
    showAlert("enter \(region.identifier)")
}

// 2. user exit region
func locationManager(manager: CLLocationManager, didExitRegion region: CLRegion) {
    showAlert("exit \(region.identifier)")
}

We added two methods for notifying user that he cross region’s boundaries. We use our alert shortcut method for visualize it. The system will fire this methods only when the boundary plus a system-defined cushion distance is crossed. This cushion prevents the user being attacked by the system from numerous enter/exit events in short time while the user is traveling close the edge of the boundary.

You should be careful with number of simultaneously tracking regions. The system limited regions count to 20 for one app. If you want to work with larger regions count you can track only this regions, which are close to user’s location right now. When the user’s location changes, you can remove regions that are now farther way and add regions coming up on the user’s path. If reach regions limit, location manager will fire monitoringDidFailForRegion method. You can handle it for better app’s UX.

Just two methods and a lot of limitation to work with them which we should always keep in mind.

We set up all methods we need for working with simple regions. Now you can go to your car and try to drive through your region. Just kidding. There is another convenient way to test this functionality.

How to Test Regions

There is the convenient way how to do it with Xcode. We will use GPX files. GPX is an XML schema described common GPS data format for using in software development. It is easier to show it than describe it.



    
    
    

In this example GPX we put three points near our region. Xcode will take it one-by-one and move user’s location. One second per one point. Please, download my GPX file America.gpx. After you downloaded GPX file just drag-and-drop it into your project.

Now we can run application, go back to Xcode → Open Debug Area → Choose “Simulate location” → Choose America. Go back to simulator, user’s location should start moving.

geotarget-simulate-location

After few seconds you will see alert “enter Lorrenzillo’s”, after few more seconds “exit Lorrenzillo’s”. So, our regions finally works! Congratulations!

geotarget-demo-app

Complex Business Logic in Your Region

For some applications enter/exit events is enough. But what if you want track more complicated logic? Maybe you are interesting how long user where in a region or user’s average speed while he was in the region. In this example we will check if user were enough time in our restaurant, which means he is a visitor and we can ask him for his feedback. Let’s add some improvements into our project.

// 1. 
var monitoredRegions: Dictionary = [:]

func locationManager(manager: CLLocationManager, didEnterRegion region: CLRegion) {
    showAlert("enter \(region.identifier)")
    
    // 2.1. Add entrance time
    monitoredRegions[region.identifier] = NSDate()
}

func locationManager(manager: CLLocationManager, didExitRegion region: CLRegion) {
    showAlert("exit \(region.identifier)")
    
    // 2.2 Remove entrance time
     monitoredRegions.removeValueForKey(region.identifier)
}

// 3. Update regions logic
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    updateRegions()
}
  1. We will store user’s entrance time in dictionary.
  2. Here I implement adding/deleting region’s entrance time.
  3. Location manager’s delegate method didUpdateLocations will help us check is user already spend enough time in our region.
func updateRegions() {
    
    // 1.
    let regionMaxVisiting = 10.0
    var regionsToDelete: [String] = []
    
    // 2.
    for regionIdentifier in monitoredRegions.keys {
        
        // 3.
        if NSDate().timeIntervalSinceDate(monitoredRegions[regionIdentifier]!) > regionMaxVisiting {
            showAlert("Thanks for visiting our restaurant")
   
            regionsToDelete.append(regionIdentifier)
        }
    }
    
    // 4.
    for regionIdentifier in regionsToDelete {
        monitoredRegions.removeValueForKey(regionIdentifier)
    }
}
  1. Let’s assume 10 seconds is enough time for user. Also, we need variable for storing regions’ where user were enough time for future deleting this regions, as they already did what we want.
  2. Now we go through all currently monitored regions.
  3. If user already enough time, we will show to user special message and mark this region as ready for deleting.
  4. And delete all regions, where user was enough time.

Now you can implement absolutely any complex login within updateRegions method.

Custom Regions

Unfortunately, Apple’s CLRegion has few limitations. And one of this that you can’t monitor region if your application has only “While in Use” location access. As we know, a lot of users aware of their battery life and they don’t want your app always track them. It’s really hard to explain to users that their life will be better if they allow your app to use location always. That’s why sometimes you want to track regions only while user run your app. I suggest to create your custom region class, with similar interface and callback methods like CLRegion. So, it will be easier to understand what you class do for you and any other develop how already familiar with CLRegion.

protocol RegionProtocol {
    var coordinate: CLLocation {get}
    var radius: CLLocationDistance {get}
    var identifier: String {get}

    func updateRegion()
}

protocol RegionDelegateProtocol {
    func didEnterRegion()
    func didExitRegion()
}

You can use this protocols for easily move from CLRegion to your custom class.

One more CLRegion limitation is that we can track only circle areas. Sometimes, we want monitor polygon area (square, pentagan, etc.) or oval area. We also can do it with our custom class. You just need to check this condition in didEnterRegion method of our RegionDelegateProtocol.

Also, it is not necessary to show some alerts to user immediately. There are lot of use cases when we want store this data for future data analysis.

For the complete Xcode project, you can get it here.

Read next