Adding maps to your app

I recently put a sample project on github for using Apple Maps on an iOS application as I wanted to play with Core Location and Maps since I only dealt with Google Maps via Javascript in my existing apps. Here is the link to the project:

https://github.com/gualtierofrigerio/GFMapKitWrapper

I’ll eventually make it available on CocoaPods so it is already made as a Pod. The interesting (at least I hope so…) part of the project is The GFMapKitWrapper framework that I’ll describe in this article.
I’ll star with a quick introduction to Core Location, then MapKit and finally the example app using my framework.

Core Location

Even if you can use a map without knowing the user location I think it is important to start by talking about Core Location, as your app will likely need to know where the user is in order to center the map on his current location and show whatever is near to him by placing a pin on the map and even providing direction to the point of interest like a shop, a restaurant etc.
I don’t want to go too much into details about Core Location, but just describe what it does and how my framework interacts with it via the class GFLocationManager

https://github.com/gualtierofrigerio/GFMapKitWrapper/blob/master/GFMapKitWrapper/Classes/GFLocationManager.swift

It is possible to check if location is available (if not you can ask your user for permission to know his location), get his location and a few utility functions to get the coordinate of an address or an array of addresses. I use CLGeocorder, part of Core Location, to translate an address to a coordinate, while I need to implement the CLLocationManagerDelegate to get the current location.


To access Core Location we need to instantiate an object CLLocationManager.
In my example I use 3 api of the location manager

  1. locationServicesEnabled: Determines whether the user has location services enabled and returns a boolean. If it returns false we ask for permission to use location services.
  2. requestWhenInUseAuthorization: ask the user to give permission to access to location data while the app is in use. There is a similar API requestAlwaysAuthorization to request the always permission, so you can access location data while running background. My class has a boolean variable so it can ask for the in use or the always permission, the default is when in use.
  3. startUpdatingLocation: as the name suggests, starts to ask for user location and sends periodically updates. To get those updates you need to implement the CLLocationManagerDelegate function didUpdateLocations

I start updating the user location when I’m asked for user current location, and stop right after I get the first value. It is also possibile to call requestLocation and get the same delegate function call a single time. There’s the possibility to set the desired accuracy, I haven’t implemented that in my class yet, but location manager allows different accuracy values. The tradeoff is between speed and accuracy, if you need to place the user on a map you may want to have a precise location, but sometimes you just need to have a vague idea of where your user lives, so the fastest way to get his location could be just fine. Look here for details

https://developer.apple.com/documentation/corelocation/cllocationaccuracy

Even if you don’t want to deal with actual location you may need to place a pin on the map, or to center the map at a particular address.
Maybe your user decided not to give permission to be tracked, but prefers to manually enter his location or chose the state, province and city from a form.
What you need is a way to convert an address to a point, specifically a coordinate, and we can use CLGeocoder for this purpose.
I use this class only once in my example, look for the function getCoordinate.


let geocoder = CLGeocoder()
geocoder.geocodeAddressString(address) { (placemarks, error) in
    if  let placemark = placemarks?[0],
        let coordinate = placemark.location?.coordinate {
            completionHandler(coordinate)
        }
        else {
            completionHandler(nil)
        }
    }

CLGeocorder returns an array of placemarks, if is isn’t empty I get the first coordinate and pass it to the completionHandler. The coordinate is always a CLLocationCoordinate2D object, containing a latitude and a longitude.

To wrap things up: GFLocationManager is a wrapper for calls to Core Location, takes care of requesting permission to track user location and provides functions to get the current location or to get the coordinate from an address.

MapKit

After the quick introduction of Core Location and the wrapper I made is time to talk about the main topic: adding a map to your iOS app.
As I stated in the first paragraph I used Google Maps in my apps in the past and I wanted to try out the fully native solution. Using Google Maps is as easy as adding a Webview, make an HTML page with the correct <script> pointing at google and dealing with message passing between the page and your code. Since I’ve built hybrid apps for a while it was very convenient for me to add another page with the map, placing it at current user location (interacting with Core Location) and get notified after a pin was selected. If your app is hybrid that’s the best way, but if you’re fully native why not using Apple Maps instead?

My github project has two classes to deal with Maps, the main one is GFMapKitWrapper, which uses GFMapKitAnnotation to make a custom annotation and place it on the map. All the logic to center the map, add annotations and draw routes is in GFMapKitWrapper, so let’s take a look at it.

https://github.com/gualtierofrigerio/GFMapKitWrapper/blob/master/GFMapKitWrapper/Classes/GFMapKitWrapper.swift

I created a struct called GFMapKitWrapperConfiguration to store some configuration parameters for my wrapper, like the color of pins, lines to draw routes, type of transportation (car, public transit, walk) and camera pitch and altitude so you can have more zoom or a different viewing angle. The second struct is GFMapKitWrapperAnnotation and is needed to add an annotation to the map, either by specifying a coordinate or an address.
Let’s the to the main class GFMapKitWrapper.
The map itself is stored into mapView and I give the possibility to create the wrapper with a map (so you can point it to an outlet in interface builder) or to let it create the map if you construct your view in code.
To deal with user location I use an instance of GFLocationManager so the MapKit wrapper is all about dealing with MapKit.
I want to talk about 3 main functionalities

  1. center the map on a coordinate
  2. draw a route between two coordinates
  3. add annotation

Center the map

I provide multiple ways to center the map, you can provide a coordinate, an address, current user location. As I said GFLocationManager takes care of that, so let’s see how to center the map given a latitude and longitude. The coordinate is specified by an object of type CLLocationCoordinate2D, consisting of two Double values representing the latitude and longitude.


private func centerMapOnCoordinate(coordinate:CLLocationCoordinate2D) {
    let region = MKCoordinateRegionMakeWithDistance(coordinate, self.configuration.regionSize, self.configuration.regionSize)
    mapView?.region = region
}

To center the map we first have to create a region with the desired coordinate and a size. I use the same configuration parameter for both the latitude and longitude span, but as you can see you can specify two different parameters.

Draw a route

Sometimes you need to give directions to a particular point of interest. You can write the route on maps, or you can open the Maps app with the directions. The latter is the simplest solution, but your user will leave your app so I suggest choosing that only if after selecting the point of interest the user is done with your app, and all he needs is having the directions to drive there.
Drawing a route on the map into your app is a way to show where the point of interest is and you can give an ETA too.
There are multiple functions to draw a route on my wrapper, but eventually each of them calls the private function I past below


private func drawRoute(fromCoordinate:CLLocationCoordinate2D, toCoordinate:CLLocationCoordinate2D) {
    let startItem = MKMapItem(placemark: MKPlacemark(coordinate: fromCoordinate))
    let destinationItem = MKMapItem(placemark: MKPlacemark(coordinate: toCoordinate))
    let request = MKDirectionsRequest()
    request.source = startItem
    request.destination = destinationItem
    request.transportType = configuration.transportType
    let directions = MKDirections(request: request)
    directions.calculate { (response, error) in
        if let response = response {
            let routes = response.routes
            if let route = routes.first {
                for step in route.steps {
                    self.mapView?.add(step.polyline, level: MKOverlayLevel(rawValue: 0)!)
                }
             }
        }
     }
}

First I need to create two MKMapItem, one for the start and one for the destination. I then create a MKDirectionsRequest with the two items and the trasportType set into the configuration.
The call to MKDirection(request:) gives us a directions object, which calculate the possible routes for our request.
In the completion handler I look for the first route, but you could draw every route on the map if you would, like some apps do to show you alternatives.
The route is a set of steps, each of them having a polyline object (a line between two points) so I add each polyline to the map.
That is not the end of it. The wrapper implements two functions of the MKMapViewDelegate protocol, one for drawing stuff on the map and the other to add annotation.
For now let’s look at the function returning a MKOverlayRenderer.


public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
    if let polyline = overlay as? MKPolyline {
        let polylineRendered = MKPolylineRenderer(polyline: polyline)
        polylineRendered.strokeColor = configuration.lineColor
        polylineRendered.lineWidth = 3.0
        return polylineRendered
    }
    return MKOverlayRenderer(overlay: overlay) // default renderer
}

This delegate function is called after an overlay object, like a polyline, is added to the map. I set the stroke color based on the configuration, but as you imagine you could implement different colour for different kind of routes.

Add annotations

Usually the main reason to embed a map into your app is to show the user some points of interest near his location. After tapping on the points in your map you can get some information and get the directions to that point, or select it and go to another view for example if you want to book a table at a restaurant or chose a delivery point.

In this article, and in my example on GitHub, I covered the simplest type of annotation: a pin opening a view with a title and a subtitle. It is possibile to customise the view by adding buttons and have a custom layout, I’ll leave that to a future commit and an update version of this article.


/* add an annotation */
public func addAnnotation(annotation:GFMapKitWrapperAnnotation) {
    if let _ = annotation.latitude, let _ = annotation.longitude {
        let gfAnnotation = GFMapKitAnnotation(annotation: annotation)
        self.mapView?.addAnnotation(gfAnnotation)
    }
    else { // if we don't have coordinate we need the address
        guard let address = annotation.address else {
            return
    }
    locationManager.getCoordinate(forAddress: address) { (coordinate) in
        if let coordinate = coordinate {
            var validAnnotation = annotation
            validAnnotation.latitude = coordinate.latitude
            validAnnotation.longitude = coordinate.longitude
            let gfAnnotation = GFMapKitAnnotation(annotation: validAnnotation)
            self.mapView?.addAnnotation(gfAnnotation)
        }
    }
 }
}

To add an annotation you can call addAnnotation with the struct GFMapKitWrapperAnnotation I defined on the same file. The struct contains a title, subtitle, the latitude and longitude or an address. If neither the coordinate nor the address is provided the annotation isn’t added to the map, we wouldn’t know where to put it after all.

If latitude and longitude aren’t set we first get them via GFLocationManager than create the GFMapKitAnnotation. The class is very simple an can be initialised with the GFMapKitWrapperAnnotation struct with valid latitude and longitude. The class has a unique identifier, necessary to deal with the MapKit delegate responsible for drawing the annotation on the map, a title and a subtitle.

To actually draw the pin on the map we need to implement the aforementioned delegate method, called viewFor Annotation


public func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
    guard let annotation = annotation as? GFMapKitAnnotation else {
        return nil // only support custom annotation class
    }
    let identifier = annotation.identifier
    var annotationView:MKPinAnnotationView?

    if let dequeuedView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MKPinAnnotationView {
        annotationView = dequeuedView
    }
    else {
        annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
    }

    annotationView?.tintColor = configuration.pinColor
    annotationView?.pinTintColor = configuration.pinColor
    annotationView?.canShowCallout = true
    return annotationView
}

As stated in the first guard let we only support our custom annotation class. The identifier is necessary to use the dequeueReusableAnnotationView, similar to the function dequeueReusableCell used in UITableView. We either use one of the dequeued views or we create a new MKPinAnnotationView, setting the color from our configuration. Setting canShowCallout to true allows the view to be displayed after a tap on the pin. As I mentioned before it is possible to customise the view even adding buttons, and there is a delegate function to be notified when the user interacts with such view.

Use the example project

I made an example project to test all the functionalities of GFMapKitWrapper, the only swift file worth mentioning is the main view controller

https://github.com/gualtierofrigerio/GFMapKitWrapper/blob/master/Example/GFMapKitWrapper/ViewController.swift

in this example I decided to go for the IBOutlet, so I put a MapView into the storyboard and pass it to GFMapKitWrapper.
You can see many commented lines, each of them to test a single function of the wrapper to center the map on a point, an address or current location and to draw a route. Finally I tested an annotation.

I will update this article every time I’ll make a significant change to the GitHub project.