Skip to main content

Building a Geo Targeting iOS App in Swift

Geo Targeting

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 Application 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:

Create an instance of locationManager, which will detect user’s location changes.
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.

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 Storyboard. We did it within the code for better visualisation.
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. authorisation 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 authorisation
else if CLLocationManager.authorizationStatus() == .AuthorizedAlways {
locationManager.startUpdatingLocation()
   }
}

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

So, which

If status is not determined, then we should ask for authorisation. We ask for “Always” permission because this is the only status which works with CLRegion monitoring.
If authorisation 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.

If we have authorisation we can start the standard location service updating.

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

Of course, there are few more statuses of the location authorisation, 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 = "City Name Here"
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.
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.

For better visualisation we added annotation in the center of our region.
For better visualisation we added circle on the map, which represent region’s boundaries.
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 visualise it. The system will fire this methods only when the boundary plus a system-defined cushion distance is crossed.

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.

<?xml version="1.0"?>
<gpx version="1.1" creator="Xcode">
<wpt lat="37.702801" lon="-121.751862"></wpt>
<wpt lat="37.702821" lon="-121.752321"></wpt>
<wpt lat="37.702840" lon="-121.752780"></wpt>
</gpx>

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.

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.

After few seconds you will see alert “enter City Name Here”, after few more seconds “exit City Name Here”. So, our regions finally works! Congratulations!


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<String, NSDate> = [:]

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()
}

We will store user’s entrance time in dictionary.
Here I implement adding/deleting region’s entrance time.
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)
       }
}

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.
Now we go through all currently monitored regions.
If user already enough time, we will show to user special message and mark this region as ready for deleting.

And delete all regions, where user was enough time.

Now you can implement absolutely any complex login within updateRegionsMethod.

Custom Regions

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, pentagon, 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.


Comments

Popular Posts

How I Reduced the Size of My React Native App by 85%

How and Why You Should Do It I borrowed 25$ from my friend to start a Play Store Developer account to put up my first app. I had already created the app, created the assets and published it in the store. Nobody wants to download a todo list app that costs 25mb of bandwidth and another 25 MB of storage space. So today I am going to share with you how I reduced the size of Tet from 25 MB to around 3.5 MB. Size Matters Like any beginner, I wrote my app using Expo, the awesome React Native platform that makes creating native apps a breeze. There is no native setup, you write javascript and Expo builds the binaries for you. I love everything about Expo except the size of the binaries. Each binary weighs around 25 MB regardless of your app. So the first thing I did was to migrate my existing Expo app to React Native. Migrating to React Native react-native init  a new project with the same name Copy the  source  files over from Expo project Install all de...

How to recover data of your Android KeyStore?

These methods can save you by recovering Key Alias and Key Password and KeyStore Password. This dialog becomes trouble to you? You should always keep the keystore file safe as you will not be able to update your previously uploaded APKs on PlayStore. It always need same keystore file for every version releases. But it’s even worse when you have KeyStore file and you forget any credentials shown in above box. But Good thing is you can recover them with certain tricks [Yes, there are always ways]. So let’s get straight to those ways. 1. Check your log files → For  windows  users, Go to windows file explorer C://Users/your PC name/.AndroidStudio1.4 ( your android studio version )\system\log\idea.log.1 ( or any old log number ) Open your log file in Notepad++ or Any text editor, and search for: android.injected.signing and if you are lucky enough then you will start seeing these. Pandroid.injected.signing.store.file = This is  file path where t...

React Native - Text Input

In this chapter, we will show you how to work with  TextInput  elements in React Native. The Home component will import and render inputs. App.js import React from 'react' ; import Inputs from './inputs.js' const App = () => { return ( < Inputs /> ) } export default App Inputs We will define the initial state. After defining the initial state, we will create the  handleEmail  and the  handlePassword  functions. These functions are used for updating state. The  login()  function will just alert the current value of the state. We will also add some other properties to text inputs to disable auto capitalisation, remove the bottom border on Android devices and set a placeholder. inputs.js import React , { Component } from 'react' import { View , Text , TouchableOpacity , TextInput , StyleSheet } from 'react-native' class Inputs extends Component { state = { ...