How to Build a Compass App in Swift šŸ§­

āš ļø This article assumes that youā€™re already somehow familiar with the Core Location Framework

Back in 2007 both Android and iOS (then iPhone OS) were at the very early stages of what has become the smartphone biggest revolution.

Along with the Android announcement, Google also launched its first Android Developer Challenge, aimed to raise developers interest to the platform.

Among the several entries to the challenge there was Enkin, essentially Google Maps on steroids. If you have 7 minutes to spare, hereā€™s the video entry:

These guys didnā€™t win the challengeā€¦they got hired by Google!

Bare in mind that, at the time, most of the world still didnā€™t know what Android or an iPhone were, we also didnā€™t have the App/Play Store yet!

After watching that video, my mind was completely blown away šŸ¤Æ

Now letā€™s skip forward to 10 years later (today!), how difficult it is to create something similar? Well, replicating the whole Enkin concept might take too long, letā€™s start with the compass: how hard it is to create a Compass app?

The Basics: iPhone Heading

Nowadays every iOS device has a magnetometer on board, that plus iOSā€™s CLLocationManager makes this first challenge incredibly easy.

One of the functionalities of CLLocationManager is reporting the device Heading.

The Heading comes in the form of a CLHeading data object which, among its properties, contains trueHeading, that is the actual device orientation relative to true north, in degrees.

Just imagine a 2D vector with origin on your home button and pointing towards the earpiece speaker: the trueHeading is the angle between that vector and the vector starting from the same point but pointing towards the true north.

If your phone points exactly to the True North youā€™ll get a heading of 0 (zero) degrees, if it points towards East youā€™ll get 90 degrees etc.

The Picture

We need a picture of something pointing, you can choose anything you want, what it matter is that the North arrow points up, this is what I use:

compass

Why it is important that it points up? Because this way, when we get a 0-degree (zero-degree) heading, we know that we donā€™t have to rotate the picture, as it is already pointing to the right direction!

If we get any other heading angle, then we know that we will need to rotate the picture exactly of the same degrees.

Code Snippets

These are the core parts for our first Compass App:

Location Manager

This is the CLLocationManager declaration, with the request to start monitoring the device heading embedded in.

let locationManager: CLLocationManager = {
  $0.requestWhenInUseAuthorization()
  $0.startUpdatingHeading()
  return $0
}(CLLocationManager())

Location Manager Delegate

As Iā€™ve said above, all we have to do is rotate our image of the same angle as our heading, I didnā€™t lie:

func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
  UIView.animate(withDuration: 0.5) {
    let angle = newHeading.trueHeading.toRadians // convert from degrees to radians
    self.imageView.transform = CGAffineTransform(rotationAngle: angle) // rotate the picture
  }
}

CGAffineTransform requires a rotationAngle expressed in radians, therefore Iā€™ve added a small extension to convert our heading angle to radians:

extension CGFloat {
  var toRadians: CGFloat { return self * .pi / 180 }
  var toDegrees: CGFloat { return self * 180 / .pi }
}

New in Swift 3.1: the Ļ€ value is defined as a static property on Double, Float, and CGFloat!

Thatā€™s it! This is all you need to build your first compass app! šŸŽ‰

Pointing At Any Direction

Our first target is complete, we can build a whole Compass App in less than 50 lines of code!

One of the cool features about Enkin is that, given the location, the App can point at things around you.

Say we have a Movie Theater that we really, really, like. However, we get lost fairly often and we always miss the first five minutes of every movie. We want to put an end to that: how can we build an app that always points at our movie theater?

Obviously knowing only the device heading is not enough anymore, to solve this new challenge we must have three parameters: our device heading, our device location, and our movie theater location.

The Parameters

Weā€™ve seen how to get the device heading and obviously we know by heart the GPS location of our beloved movie theater.

To also keep track of our location we will need to update our Location Manager declaration to request not only the device heading, but also our device location:

let locationManager: CLLocationManager = {
  $0.requestWhenInUseAuthorization()
  $0.startUpdatingLocation() // now we request to monitor the device location!
  $0.startUpdatingHeading()
  return $0
}(CLLocationManager())

All itā€™s left to do now is to declare and fill in our delegateā€™s locationManager(_:, didUpdateLocations:) function, then we will have all our parameters!

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
  guard let currentLocation = locations.last else { return }
  lastLocation = currentLocation // store this location somewhere
}

Rotate The (Compass) Picture

Now that have our three parameters, we need to rotate our picture accordingly to where the movie theater is.

In order to do that, first we must compute the Bearing angle between our location and the Movie Theater.

In this case saying ā€œA picture is worth a thousand wordsā€ really shines, this is what the Bearing between two locations is:

Note: Ī² is our Bearing

bearing-wiki-picture

Picture shamelessly stolen from StackOverflow.

If your phone points exactly to the true north, the Bearing is the angle that your device must rotate in order to point right to our movie theater.

In order to compute our bearing, we must project our GPS locations (the deviceā€™s and the theater's) into a 2D plane, and then apply the formula above.

Thereā€™s no definitive way to map each GPS location to a 2D panel point, this is why we have so many different world map projections. Luckily, in our case any projection will do.

There are plenty of solutions for the bearing problem on StackOverflow, I just pick one and went on with the rest of the project:

public extension CLLocation {
  func bearingToLocationRadian(_ destinationLocation: CLLocation) -> CGFloat {
    
    let lat1 = self.coordinate.latitude.degreesToRadians
    let lon1 = self.coordinate.longitude.degreesToRadians
    
    let lat2 = destinationLocation.coordinate.latitude.degreesToRadians
    let lon2 = destinationLocation.coordinate.longitude.degreesToRadians
    
    let dLon = lon2 - lon1
    
    let y = sin(dLon) * cos(lat2)
    let x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon)
    let radiansBearing = atan2(y, x)
    
    return CGFloat(radiansBearing)
  }
  
  func bearingToLocationDegrees(destinationLocation: CLLocation) -> CGFloat {
    return bearingToLocationRadian(destinationLocation).radiansToDegrees
  }
}

This piece of code takes care of both the projection and the bearing computation. Credits to Fabrizio Bartolomucci.

We can now finally update our image rotation with the device latest heading and bearing:

UIView.animate(withDuration: 0.5) {
  self.imageView.transform = CGAffineTransform(rotationAngle: latestBearing - latestHeading)
}

Thatā€™s it! No more arriving late to the movies! šŸ˜†

Code Snippet

Weā€™ve seen quite a bit in this post and, because of this, Iā€™ve decided to open source the whole Compass app.

compass-github

Click on the picture to jump to the repository.

Once launched, the app will point you towards the North, you can tap anywhere and a World map will be shown: tap wherever youā€™d like and the compass will start pointing at that new location.

Beside what weā€™ve seen in this post, the app also handles interface rotation (landscape left/right, portrait ..) and device rotation (a.k.a. device upside down etc). Check out the source code!

The app is also available on the App Store (because..why not?), completely free of charge.

Thatā€™s it for today, happy coding!

ā­‘ā­‘ā­‘ā­‘ā­‘