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.
- 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.
- 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.
- 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.
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
- center the map on a coordinate
- draw a route between two coordinates
- 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.
/* 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)
}
}
}
}
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
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.