MapKit Tutorial: Overlay Views

Niv Yahel
Learn how to add an overlay image using MapKit!

Learn how to add an overlay image using MapKit!

Update 04/21/2015: Updated for Xcode 6.3 / Swift 1.2

It’s quite easy to add a map into your app using MapKit. But what if you want to decorate or customize the map provided by Apple with your own annotations and images?

Luckily, Apple provides and easy way to do this with custom overlay views.

In this tutorial, you’ll create an app for the Six Flags Magic Mountain amusement park. If you’re a roller coaster fan in the LA area, you’ll be sure to appreciate this app! :]

Just think about what would interest a visitor to the park that isn’t in the standard satellite or map view. Things like the location of specific attractions, routes to the various rides and roller coasters, and the location of characters around the park are all perfect candidates for custom overlay images – and that’s exactly what you’ll be adding in this tutorial.

Keep reading to add some excitement to these vanilla maps!

Note: You have two options for how to proceed with this tutorial based on your experience level:

  1. Already familiar with MapKit? If you’re already familiar with MapKit and want to dive right into the overlay image code, you can skip (or scan) ahead to the “All About Overlay Views” section – there’s a starter project waiting for you there.
  2. New to MapKit? If you are new to MapKit, keep reading and I’ll walk you through adding map into your app from the very beginning!

Getting Started

To get started, download the starter project which provides you with a basic application for both the iPhone and the iPad, with some rudimentary navigation included – but no maps yet!

The interface provided in the starter app contains a UISegmentedControl to switch between the different map types you will implement, and an action button which presents a table of options to allow you to choose what map features to display. You select or deselect options by tapping them, and tapping the Done button will dismiss the options list.

The class MapOptionsViewController drives the options view, which defines an important enum that you’ll use later on. The rest of the code in this class is outside the scope of this tutorial. However, if you need more information on UITableView, check out one of the many UITableView tutorials to quickly get up to speed.

Open up the starter project in Xcode, and build and run. I bet you didn’t expect that so soon in the tutorial! You’ll see the following:

Park view app screenshots

As advertised, the starter app is pretty basic. If your map application is going to do anything useful, then it’s going to need a map, for starters.

Adding A Map View

To add a MapView to your app, start by opening MainStoryboard_iPhone.storyboard. Select the Park Map View Controller scene, and drop a Map View object on the view. Position the MapView so it fills the entire view, as shown below:

Park view app screenshots

Now open up MainStoryboard_iPad.storyboard, add a MapView as above, and again adjust it so it fills the entire view.

Resist the temptation to build and run the app at this point to see what it looks like with your map added; however, if you did, you would see the following exception and your app will crash:

*** Terminating app due to uncaught exception 'NSInvalidUnarchiveOperationException', reason: 'Could not instantiate class named MKMapView'

This is because you haven’t yet linked the MapKit.framework to your target!

Wiring Up Your MapView

You’ll be writing a whole lot of MapKit code, so the easiest way to get the framework into the build is to reference it from code. Open ParkMapViewController.swift and add the following import to the top of the file:

import MapKit

This will make MapKit available within this source file, and also include it into the project as a whole.

To do anything with a MapView, you need to do two things – associate it with an outlet, and register the view controller as the MapView’s delegate.

Next, open MainStoryboard_iPhone.storyboard and make sure your Assistant Editor is open with ParkMapViewController.swift visible. Then control-drag from the map view to above the first outlet, as shown below:

Add MapView Outlet

In the popup that appears, name the outlet mapView, and click Connect.

Now you need to set the delegate for your map view. To do this, right-click on the map view object to open the context menu, then drag the delegate outlet to Park Map View Controller, as shown below:

Set MapView Delegate

Now perform the same steps for your iPad storyboard — connect the MapView to the mapView outlet (but this time drag on top of the existing outlet rather than creating a new one), and make the view controller the MapView’s delegate.

Now having finished wiring your outlets, you need to indicate that ParkMapViewController conforms to the MKMapViewDelegate protocol. Open ParkMapViewController.swift and add the following to the very end of the file, outside the class declaration curly braces:

// MARK: - Map View delegate

extension ParkMapViewController: MKMapViewDelegate {
}

You’ll fill in the delegate methods later. But for now, that’s it for setting your outlets, delegates, and controllers.

Build and run to check out your snazzy new map! It should look like the screenshot below:

Park Map View Map Example

As you can see, it doesn’t take much work to add a map to your app.

As cool as it is to have a map in your application, it would be even cooler if you could actually do something with the map! :] It’s time to start adding some interactions to your map!

Interacting With MKMapView

Although the default view of the map looks nice, it’s a little too broad to be of use since it is the theme park that interests the user, not the entire continent! It would make more sense to center the map view on the park when you launch the app.

There are many ways to provide position information for a specific location; you could fetch it from a web service, or you could simply package it and ship it with the app itself.

To keep things simple, you will include the location information for the park with your app in this tutorial. Download the resources for this project, which contains a file named MagicMountain.plist with the park information:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>midCoord</key>
        <string>{34.4248,-118.5971}</string>
        <key>overlayTopLeftCoord</key>
        <string>{34.4311,-118.6012}</string>
        <key>overlayTopRightCoord</key>
        <string>{34.4311,-118.5912}</string>
        <key>overlayBottomLeftCoord</key>
        <string>{34.4194,-118.6012}</string>
        <key>boundary</key>
        <array>
                <string>{34.4313,-118.59890}</string>
                <string>{34.4274,-118.60246}</string>
                <string>{34.4268,-118.60181}</string>
                <string>{34.4202,-118.6004}</string>
                <string>{34.42013,-118.59239}</string>
                <string>{34.42049,-118.59051}</string>
                <string>{34.42305,-118.59276}</string>
                <string>{34.42557,-118.59289}</string>
                <string>{34.42739,-118.59171}</string>
        </array>
</dict>
</plist>

This file contains the information that you need to center the map on the park, and it also contains boundary information for the park which you’ll use a bit later.

All information in this file is in the form of latitude/longitude coordinates.

Add this file to your project by dragging it to the Park Information group and choosing to copy it to the project.

Now that you have some geographical information about the park, you should model it into a Swift class in order to work with it in your app.

Right-click the Models group, and choose New File… Select the iOS\Source\Swift File template for the file and name it Park.

Once you create the new class, open Park.swift and replace its contents with the following:

import Foundation
import MapKit

class Park {
  var boundary: [CLLocationCoordinate2D]
  var boundaryPointsCount: NSInteger
  
  var midCoordinate: CLLocationCoordinate2D
  var overlayTopLeftCoordinate: CLLocationCoordinate2D
  var overlayTopRightCoordinate: CLLocationCoordinate2D
  var overlayBottomLeftCoordinate: CLLocationCoordinate2D
  var overlayBottomRightCoordinate: CLLocationCoordinate2D
  
  var overlayBoundingMapRect: MKMapRect
  
  var name: String?
}

Most of these properties should look familiar, as they have counterparts in the plist file above.

Now, having defined the properties of the class, you need to add an initializer, which will allow you to give the defined properties a value. The initializer, init(filename:), will read all of the information from the plist file into the defined properties. This will be pretty straightforward if you have done any file I/O with property lists.

Add the following code to the Park class:

init(filename: String) {
  let filePath = NSBundle.mainBundle().pathForResource(filename, ofType: "plist")
  let properties = NSDictionary(contentsOfFile: filePath!)
    
  let midPoint = CGPointFromString(properties!["midCoord"] as! String)
  midCoordinate = CLLocationCoordinate2DMake(CLLocationDegrees(midPoint.x), CLLocationDegrees(midPoint.y))
    
  let overlayTopLeftPoint = CGPointFromString(properties!["overlayTopLeftCoord"] as! String)
  overlayTopLeftCoordinate = CLLocationCoordinate2DMake(CLLocationDegrees(overlayTopLeftPoint.x),
    CLLocationDegrees(overlayTopLeftPoint.y))
    
  let overlayTopRightPoint = CGPointFromString(properties!["overlayTopRightCoord"] as! String)
  overlayTopRightCoordinate = CLLocationCoordinate2DMake(CLLocationDegrees(overlayTopRightPoint.x),
    CLLocationDegrees(overlayTopRightPoint.y))
    
  let overlayBottomLeftPoint = CGPointFromString(properties!["overlayBottomLeftCoord"] as! String)
  overlayBottomLeftCoordinate = CLLocationCoordinate2DMake(CLLocationDegrees(overlayBottomLeftPoint.x),
      CLLocationDegrees(overlayBottomLeftPoint.y))
    
  let boundaryPoints = properties!["boundary"] as! NSArray
    
  boundaryPointsCount = boundaryPoints.count
    
  boundary = []
    
  for i in 0...boundaryPointsCount-1 {
    let p = CGPointFromString(boundaryPoints[i] as! String)
    boundary += [CLLocationCoordinate2DMake(CLLocationDegrees(p.x), CLLocationDegrees(p.y))]
  }
}
Note: properties reads the contents of the file as defined by filePath as a NSDictionary. Each element’s type is AnyObject. To convert the values into Strings, you must downcast them by adding as String when accessing the NSDictionary‘s elements.

The code above uses CLLocationCoordinate2DMake() to make a CLLocationCoordinate2D structure using latitude and longitude coordinates. MapKit API’s use CLLocationCoordinate2D structures to represent geographical locations.

This initializer also creates a CLLocationCoordinate2D array that will be used later to display the park boundary.

One property missing from the property list file is overlayBottomRightCoordinate. The plist file provides coordinates for the other three corners (top right, top left, and bottom left), but not the bottom right. Why?

The reason is that you can calculate this final corner of the rectangle based on the other three points — it would be redundant to include this information when you can calculate it.

Replace the current declaration of overlayBottomRightCoordinate inside of Park.swift with the following code in order to implement the calculated bottom right coordinate:

var overlayBottomRightCoordinate: CLLocationCoordinate2D {
  get {
    return CLLocationCoordinate2DMake(overlayBottomLeftCoordinate.latitude,
      overlayTopRightCoordinate.longitude)
  }
}

This getter method generates the bottom right coordinate using the bottom left and top right coordinates.

Finally, you’ll need a method to create a bounding box based on the coordinates read in above.

Replace the definition of overlayBoundingMapRect inside of Park.swift with the following:

var overlayBoundingMapRect: MKMapRect {
  get {
    let topLeft = MKMapPointForCoordinate(overlayTopLeftCoordinate)
    let topRight = MKMapPointForCoordinate(overlayTopRightCoordinate)
    let bottomLeft = MKMapPointForCoordinate(overlayBottomLeftCoordinate)
      
    return MKMapRectMake(topLeft.x,
      topLeft.y,
      fabs(topLeft.x-topRight.x),
      fabs(topLeft.y - bottomLeft.y))
  }
}

This getter method generates an MKMapRect object, which is a bounding rectangle for the park’s boundaries. It’s really just a rectangle that defines how big the park is, based on the provided coordinates, and is centered on the midpoint of the park.

Now it’s time to put this new class to use. Open ParkMapViewController.swift and add the following property to the class:

var park = Park(filename: "MagicMountain")

This will initialize the park property using MagicMountain.plist by default. Then change viewDidLoad as follows:

override func viewDidLoad() {
  super.viewDidLoad()
    
  let latDelta = park.overlayTopLeftCoordinate.latitude -
    park.overlayBottomRightCoordinate.latitude
    
  // think of a span as a tv size, measure from one corner to another
  let span = MKCoordinateSpanMake(fabs(latDelta), 0.0)
    
  let region = MKCoordinateRegionMake(park.midCoordinate, span)
    
  mapView.region = region
}

The code now creates a latitude delta, which is the distance from the top left coordinate of the park’s property to the bottom right coordinate of the park’s property.

You can then use the latitude delta to generate an MKCoordinateSpan struct which defines the area spanned by a map region.

MKCoordinateSpan is then used along with the park’s midCoordinate property (which is just the midpoint of the park’s bounding rectangle) to create an MKCoordinateRegion. This MKCoordinateRegion structure is then used to position the map in the map view using the region property.

Build and run your app. Notice that the app centers the map right on the Six Flags Magic Mountain Park, just as in the image below:

Okay! Now you centered the map on the park, which is great. But the display doesn’t look terribly exciting. It’s just a big beige blank spot with a few streets on the edges.

If you’ve ever played with the Maps app, you know that the satellite imagery looks pretty cool. You can easily leverage the same satellite data in your app to dress it up a little!

Switching The Map Type

In ParkMapViewController.swift, you will find a method at the bottom that looks like the following:

@IBAction func mapTypeChanged(sender: AnyObject) {
  // To be implemented
}

Hmm, that’s a pretty ominous-sounding comment in there! :]

Fortunately, the starter project has much of what you’ll need to flesh out this method. Did you note the segmented control sitting above the map view that seems to be doing a whole lot of nothing?

That segmented control is actually calling mapTypeChanged(_:), but as you can see above, the method does nothing — yet!

Add the following implementation to mapTypeChanged():

let mapType = MapType(rawValue: mapTypeSegmentedControl.selectedSegmentIndex)
switch (mapType!) {
case .Standard:
  mapView.mapType = MKMapType.Standard
case .Hybrid:
  mapView.mapType = MKMapType.Hybrid
case .Satellite:
  mapView.mapType = MKMapType.Satellite
}

Believe it or not, adding standard, satellite, and hybrid map types to your app is as simple as a switch statement on mapTypeSegmentedControl.selectedSegmentIndex, as seen in the code above! Wasn’t that easy?

Build and run your app. Using the segmented control at the top of the screen, you should be able to flip through the map types, as seen below:

Even though the satellite view still is much better than the standard map view, it’s still not very useful to your park visitors. There’s nothing labeled — how will your users find anything in the park?

One obvious way is to drop a UIView on top of the map view, but you can take it a step further and instead leverage the magic of MKOverlayRenderer to do a lot of the work for you!

All About Overlay Views

Note: If you’re skipping ahead from earlier in this tutorial, you can pick up at this point with this starter project. Also, you should download the resources for this project which you’ll be adding in as you go.

Before you start creating your own views, you need to understand the classes that make this all possible – MKOverlay and MKOverlayRenderer.

A MKOverlay is how you tell MapKit where you want the overlays drawn. There are three steps to using the class:

  1. Create your own custom class that implements the MKOverlay protocol, which has two required properties: coordinate and boundingMapRect. These two properties define where the overlay resides on the map, as well as its size.
  2. Create an instance of this class for each area for which you want to display an overlay. For example, in this app, you might create one instance for a rollercoaster overlay, and one for a restaurant overlay.
  3. Finally, add the overlays to your Map View.

Now the Map View knows where it’s supposed to display overlays – but how does it know what to display in each region?

Enter MKOverlayRenderer. You create a subclass of this to set up what you want to display in each spot. For example, in this app you’ll just draw an image of the rollercoaster or restaurant.

A MKOverlayRenderer is really just a special kind of UIView, as it inherits from UIView. However, you don’t add an MKOverlayRenderer directly to the MKMapView. This is an object the MapKit framework expects you to provide. After you give it to MapKit, it will render it as an overlay on top of the map.

Remember how a map view has a delegate – and you set it to your view controller in this tutorial? Well, there’s a delegate method you implement to return an overlay view:

func mapView(mapView: MKMapView!, rendererForOverlay overlay: MKOverlay!) -> MKOverlayRenderer!

MapKit will call this method when it realizes there is an MKOverlay object in the region that the map view’s viewport is displaying.

To sum it up, you don’t add MKOverlayRenderer objects directly to the map view; rather, you tell the map about MKOverlay objects to display and return them when the delegate method requests them.

Now that you’ve covered the theory, it’s time to put those concepts to use!

Adding Your Own Information

As you saw earlier, the satellite view still doesn’t provide enough information about the park. Your task is to create an object that represents an overlay for the entire park to dress it up a little.

Select the Overlays group and create a new Swift file named ParkMapOverlay. Then replace ParkMapOverlay.swift with the following:

import UIKit
import MapKit

class ParkMapOverlay: NSObject, MKOverlay {
  var coordinate: CLLocationCoordinate2D
  var boundingMapRect: MKMapRect

  init(park: Park) {
    boundingMapRect = park.overlayBoundingMapRect
    coordinate = park.midCoordinate
  }
}

In the code above, you import the MapKit header to get those classes in scope. Conforming to the MKOverlay means you also have to inherit from NSObject. Finally, the initializer simply takes the properties from the passed Park object, and sets them to the corresponding MKOverlay properties.

Now you need to create a view class derived from the MKOverlayRenderer class.

Create a new Swift file in the Overlays group called ParkMapOverlayView. Open the file and replace its contents with the following:

import UIKit
import MapKit

class ParkMapOverlayView: MKOverlayRenderer {
  var overlayImage: UIImage
  
  init(overlay:MKOverlay, overlayImage:UIImage) {
    self.overlayImage = overlayImage
    super.init(overlay: overlay)
  }
  
  override func drawMapRect(mapRect: MKMapRect, zoomScale: MKZoomScale, inContext context: CGContext!) {
    let imageReference = overlayImage.CGImage
    
    let theMapRect = overlay.boundingMapRect
    let theRect = rectForMapRect(theMapRect)
    
    CGContextScaleCTM(context, 1.0, -1.0)
    CGContextTranslateCTM(context, 0.0, -theRect.size.height)
    CGContextDrawImage(context, theRect, imageReference)
  }
}

The implementation here contains two methods and a UIImage property for the overlay image itself.

init(overlay:overlayImage:) effectively overrides the base method init(overlay:) by providing a second argument overlayImage. The passed image is stored in the class extension property that appears in the next method, drawMapRect.

drawMapRect is the real meat of this class; it defines how MapKit should render this view when given a specific MKMapRect, MKZoomScale, and the CGContextRef of the graphic context, with the intent to draw the overlay image onto the context at the appropriate scale.

Details on Core Graphics drawing is quite far out of scope for this tutorial. However, you can see that the code above uses the passed MKMapRect to get a CGRect, in order to determine the location to draw the CGImageRef of the UIImage on the provided context. If you want to learn more about Core Graphics, check out our Core Graphics tutorial series.

Okay! Now that you have both an MKOverlay and MKOverlayRenderer, you can add them to your map view.

In ParkMapViewController.swift, add the following method to the class:

func addOverlay() {
  let overlay = ParkMapOverlay(park: park)
  mapView.addOverlay(overlay)
}

This method will add an MKOverlay to the map view.

If the user should choose to show the map overlay, then loadSelectedOptions() should call addOverlay(). Update loadSelectedOptions() with the following code:

func loadSelectedOptions() {
  mapView.removeAnnotations(mapView.annotations)
  mapView.removeOverlays(mapView.overlays)
  
  for option in selectedOptions {
    switch (option) {
    case .MapOverlay:
      addOverlay()
    default:
      break;
    }
  }
}

Whenever the user dismisses the options selection view, the app calls loadSelectedOptions(), which then determines the selected options, and calls the appropriate methods to render those selections on the map view.

loadSelectedOptions() also removes any annotations and overlays that may be present so that you don’t end up with duplicate renderings. This is not necessarily efficient, but it is a simple approach for the purposes of this tutorial.

To implement the delegate method, add the following method to the MKMapViewDelegate extension at the bottom of the file:

func mapView(mapView: MKMapView!, rendererForOverlay overlay: MKOverlay!) -> MKOverlayRenderer! {
  if overlay is ParkMapOverlay {
    let magicMountainImage = UIImage(named: "overlay_park")
    let overlayView = ParkMapOverlayView(overlay: overlay, overlayImage: magicMountainImage!)
    
    return overlayView
  } 
  
  return nil
}

When the app determines that an MKOverlay is in view, the map view calls the above method as the delegate. The method here then returns a MKOverlayRenderer for the matching MKOverlay.

In this case, you check to see if the overlay is of the class type ParkMapOverlay; if so, you load the overlay image, create a ParkMapOverlayView instance with the overlay image, and then return this instance to the caller.

There’s one little piece missing, though — where does that suspicious little overlay_park image come from?

That’s a PNG file whose purpose is to overlay the map view for the defined boundary of the park. The overlay_park image (from the resources for this tutorial you’ve already downloaded) looks like this:

Add both the non-retina and retina images to your project under the Images group.

Build and run, choose the Map Overlay option, and voila! There’s the park overlay drawn on top of your map, just like in the screenshot below:

Zoom in, zoom out, and move around as much as you want — the overlay scales and moves as you would expect. Cool!

Annotations

If you’ve ever searched for a location in the Maps app, then you’ve seen those colored pins that appear on the map. These are known as annotations, which are created with MKAnnotationView. You can use annotations in your own app — and you can use any image you want, not just pins!

Annotations will be useful in your app to help point out specific attractions to the park visitors. Annotation objects work similarly to MKOverlay and MKOverlayRenderer, but instead you will be working with MKAnnotation and MKAnnotationView.

Create a new Swift fise in the Annotations group called AttractionAnnotation and open the new file. Replace its contents with the following:

import UIKit
import MapKit

enum AttractionType: Int {
  case AttractionDefault = 0
  case AttractionRide
  case AttractionFood
  case AttractionFirstAid
}

class AttractionAnnotation: NSObject, MKAnnotation {
  var coordinate: CLLocationCoordinate2D
  var title: String
  var subtitle: String
  var type: AttractionType
  
  init(coordinate: CLLocationCoordinate2D, title: String, subtitle: String, type: AttractionType) {
    self.coordinate = coordinate
    self.title = title
    self.subtitle = subtitle
    self.type = type
  }
}

Here you first define an enum for AttractionType to help you categorize each attraction into a type. This enum lists four types of annotations: default, rides, foods and first aid.

Next you declare that this class conforms to the MKAnnotation Protocol. Much like MKOverlay, MKAnnotation has a required coordinate property. You define a handful of properties specific to this implementation. Lastly, you define an initializer that allows you to assign values to each of the properties.

Now you need to create a specific instance of MKAnnotation to use for your annotations.

Create another Swift file called AttractionAnnotationView under the Annotations group. Open the file and replace its contents with the following:

import UIKit
import MapKit

class AttractionAnnotationView: MKAnnotationView {
  // Required for MKAnnotationView
  required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
  }
  
  // Called when drawing the AttractionAnnotationView
  override init(frame: CGRect) {
    super.init(frame: frame)
  }
  
  override init(annotation: MKAnnotation, reuseIdentifier: String) {
    super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
    let attractionAnnotation = self.annotation as! AttractionAnnotation
    switch (attractionAnnotation.type) {
    case .AttractionFirstAid:
      image = UIImage(named: "firstaid")
    case .AttractionFood:
      image = UIImage(named: "food")
    case .AttractionRide:
      image = UIImage(named: "ride")
    default:
      image = UIImage(named: "star")
    }
  }
}

MKAnnotationView requires two defined initializers: init(coder:) and init(frame:). Without their definition, errors will prevent you from building and running the app. To prevent this, simply define them and call their superclass initializers. Here, you also override init(annotation:reuseIdentifier:) based on the annotation’s type property, you set a different image on the image property of the annotation.

Great! Now having created the annotation and its associated view, you can start adding them to your map view!

First you’ll first need a few resource files that you referenced in init(annotation:reuseIdentifier:) (included in annotation-images.zip). They’re included in the resources ZIP file you’ve already downloaded; unpack annotation-images.zip and drag the images inside to the images group in your project. Also bring MagicMountainAttractions.plist into the Park Information group in your project in the same way.

For the curious among you, the plist file contains coordinate information and other details about the attractions at the park, such as the following:

<dict>
  <key>name</key>
  <string>Cold Stone</string>
  <key>location</key>
  <string>{34.42401,-118.59495}</string>
  <key>type</key>
  <string>2</string>
  <key>subtitle</key>
  <string>Cost: $</string>
</dict>

Now that you have the above resource files in place, you can leverage your new annotations!

Go back to ParkMapViewController.swift and insert the method below to add the attraction annotations to the map view.

func addAttractionPins() {
  let filePath = NSBundle.mainBundle().pathForResource("MagicMountainAttractions", ofType: "plist")
  let attractions = NSArray(contentsOfFile: filePath!)
  for attraction in attractions! {
    let point = CGPointFromString(attraction["location"] as! String)
    let coordinate = CLLocationCoordinate2DMake(CLLocationDegrees(point.x), CLLocationDegrees(point.y))
    let title = attraction["name"] as! String
    let typeRawValue = (attraction["type"] as! String).toInt()!
    let type = AttractionType(rawValue: typeRawValue)!
    let subtitle = attraction["subtitle"] as! String
    let annotation = AttractionAnnotation(coordinate: coordinate, title: title, subtitle: subtitle, type: type)
    mapView.addAnnotation(annotation)
  }
}

This method reads MagicMountainAttractions.plist and enumerates over the array of dictionaries. For each entry, it creates an instance of AttractionAnnotation with the attraction’s information, and then adds each annotation to the map view.

Now you need to update loadSelectedOptions() to accommodate this new option and execute your new method when the user selects it.

Modify loadSelectedOptions() (still in ParkMapViewController.swift) as shown below:

func loadSelectedOptions() {
  mapView.removeAnnotations(mapView.annotations)
  mapView.removeOverlays(mapView.overlays)
  
  for option in selectedOptions {
    switch (option) {
      case .MapOverlay:
        addOverlay()
      case .MapPins:
        addAttractionPins()
      default:
        break;
    }
  }
}

In addition to the overlays, you’re also hiding and showing the pins as required by calling removeOverlays or your new addAttractionPins() method.

You’re almost there! Last but not least, you need to implement another delegate method that provides the MKAnnotationView instances to the map view so that it can render them on itself.

Open ParkMapViewController.swift and add the following method to the MKMapViewDelegate class extension at the bottom of the file:

func mapView(mapView: MKMapView!, viewForAnnotation annotation: MKAnnotation!) -> MKAnnotationView! {
  let annotationView = AttractionAnnotationView(annotation: annotation, reuseIdentifier: "Attraction")
  annotationView.canShowCallout = true
  return annotationView
}

This method receives the selected MKAnnotation, and uses it to create the AttractionAnnotationView. The property canShowCallout is set to true so that when the user touches the annotation, a call-out appears with more information. Finally, the method returns the annotation view.

Build and run to see your annotations in action!

Turn on the Attraction Pins to see the result as in the screenshot below:

The Attraction pins are looking rather…sharp at this point! :]

So far you’ve covered a lot of complicated bits of MapKit, including overlays and annotations. But what if you need to use some drawing primitives, like lines, shapes, and circles?

The MapKit framework also gives you the ability to draw directly on a map view! MapKit provides MKPolyline, MKPolygon, and MKCircle for just this purpose.

I Walk The Line – MKPolyline

If you’ve ever been to Magic Mountain, you know that the Goliath hypercoaster is an incredible ride, and some riders like to make a beeline for it once they walk in the gate! :]

To help out these riders, you’ll plot a path from the entrance of the park to the Goliath.

MKPolyline is a great solution for drawing a path that connects multiple points, such as plotting a non-linear route from point A to point B. You’ll use MKPolyline in your app to draw the route that the Goliath fans should follow to ensure they get to the ride as quickly as possible!

To draw a polyline, you need a series of longitude and latitude coordinates in the order that the code should plot them. Order really is important here — otherwise, you’ll have a meandering mess of connected points, which won’t do your riders any good!

The resources for this tutorial contains a file called EntranceToGoliathRoute.plist that contains the path information, so add it to your project.

The plist has an array of coordinates like this:

<string>{34.42367,-118.594836}</string>
<string>{34.423597,-118.595205}</string>
<string>{34.423004,-118.59537}</string>

These are the latitude and longitude coordinates of each of the points in the path.

Now you need a way to read in that plist file and create the route for the riders to follow.

Open ParkMapViewController.swift and add the following method to the class:

func addRoute() {
  let thePath = NSBundle.mainBundle().pathForResource("EntranceToGoliathRoute", ofType: "plist")
  let pointsArray = NSArray(contentsOfFile: thePath!)
    
  let pointsCount = pointsArray!.count
    
  var pointsToUse: [CLLocationCoordinate2D] = []
    
  for i in 0...pointsCount-1 {
    let p = CGPointFromString(pointsArray![i] as! String)
    pointsToUse += [CLLocationCoordinate2DMake(CLLocationDegrees(p.x), CLLocationDegrees(p.y))]
  }
    
  let myPolyline = MKPolyline(coordinates: &pointsToUse, count: pointsCount)
    
  mapView.addOverlay(myPolyline)
}

This method reads EntranceToGoliathRoute.plist, and enumerates over the contained array where it converts the individual coordinate strings to CLLocationCoordinate2D structures.

It’s remarkable how simple it is to implement your polyline in your app; you simply create an array containing all of the points, and pass it to MKPolyline! It doesn’t get much easier than that.

Now you need to add an option to allow the user to turn the polyline path on or off.

Update loadSelectedOptions() to match the code below:

func loadSelectedOptions() {
  mapView.removeAnnotations(mapView.annotations)
  mapView.removeOverlays(mapView.overlays)
    
  for option in selectedOptions {
    switch (option) {
      case .MapOverlay:
        addOverlay()
      case .MapPins:
        addAttractionPins()
      case .MapRoute:
        addRoute()
      default:
        break;
    }
  }
}

The new case here adds the .MapRoute value and the addRoute() method you just added.

Finally, to tie it all together, you need to update the delegate method so that it returns the actual view you want to render on the map view.

Update mapView(_:rendererForOverlay) to handle the case of a polyline overview, as follows:

func mapView(mapView: MKMapView!, rendererForOverlay overlay: MKOverlay!) -> MKOverlayRenderer! {
  if overlay is ParkMapOverlay {
    let magicMountainImage = UIImage(named: "overlay_park")
    let overlayView = ParkMapOverlayView(overlay: overlay, overlayImage: magicMountainImage!)
      
    return overlayView
  } else if overlay is MKPolyline {
    let lineView = MKPolylineRenderer(overlay: overlay)
    lineView.strokeColor = UIColor.greenColor()
      
    return lineView
  }
    
  return nil
}

The change here is the additional else if branch to look for MKPolyline objects. The process of displaying the polyline view is very similar to previous overlay views. However, in this case, you do not need to create any custom view objects. You simply use the MKPolyLineRenderer framework provided, and initialize a new instance with the overlay.

MKPolyLineRenderer also provides you with the ability to change certain attributes of the polyline. In this case, you’ve modified the stroke color to show as green.

Build and run your app, enable the route option, and it should appear on the screen as in the screenshot below:

Goliath fanatics will now be able to make it to the coaster in record time! :]

It would be nice to show the park patrons where the actual park boundaries are, as the park doesn’t actually occupy the entire space shown on the screen.

Although you could use MKPolyline to draw a shape around the park boundaries, MapKit provides another class that is specifically designed to draw closed polygons: MKPolygon.

Don’t Fence Me In – MKPolygon

MKPolygon is remarkably similar to MKPolyline, except that the first and last points in the set of coordinates are connected to each other to create a closed shape.

You’ll create an MKPolygon as an overlay that will show the park boundaries. The park boundary coordinates are already defined in MagicMountain.plist; go back and look at init(filename:) to see where the boundary points are read in from the plist file.

Add the following method to ParkMapViewController.swift:

func addBoundary() {
  let polygon = MKPolygon(coordinates: &park.boundary, count: park.boundaryPointsCount)
  mapView.addOverlay(polygon)
}

The implementation of addBoundary above is pretty straightforward. Given the boundary array and point count from the park instance, you can quickly and easily create a new MKPolygon instance!

Can you guess the next step here? It’s very similar to what you did for MKPolyline above.

Yep, that’s right — update loadSelectedOptions to handle the new option of showing or hiding the park boundary, as shown below:

func loadSelectedOptions() {
  mapView.removeAnnotations(mapView.annotations)
  mapView.removeOverlays(mapView.overlays)
    
  for option in selectedOptions {
    switch (option) {
      case .MapOverlay:
        addOverlay()
      case .MapPins:
        addAttractionPins()
      case .MapRoute:
        addRoute()
      case .MapBoundary:
        addBoundary()
      default:
        break;
    }
  }
}

Notice the new case for .MapBoundary with the call to your new addBoundary() method.

MKPolygon conforms to MKOverlay just as MKPolyline does, so you need to update the delegate method again.

Update the delegate method in ParkMapViewController.swift as follows:

func mapView(mapView: MKMapView!, rendererForOverlay overlay: MKOverlay!) -> MKOverlayRenderer! {
  if overlay is ParkMapOverlay {
    let magicMountainImage = UIImage(named: "overlay_park")
    let overlayView = ParkMapOverlayView(overlay: overlay, overlayImage: magicMountainImage!)
      
    return overlayView
  } else if overlay is MKPolyline {
    let lineView = MKPolylineRenderer(overlay: overlay)
    lineView.strokeColor = UIColor.greenColor()
      
    return lineView
  } else if overlay is MKPolygon {
    let polygonView = MKPolygonRenderer(overlay: overlay)
    polygonView.strokeColor = UIColor.magentaColor()
      
    return polygonView
  }
    
  return nil
}

The update to the delegate method is as straightforward as before. You create an MKOverlayView as an instance of MKPolygonRenderer, and set the stroke color to magenta.

Run the app to see your new boundary in action!

That takes care of polylines and polygons. The last drawing method to cover is drawing circles as an overlay, which is neatly handled by MKCircle.

Circle In The Sand – MKCircle

MKCircle is again very similar to MKPolyline and MKPolygon, except that it draws a circle, given a coordinate point as the center of the circle, and a radius that determines the size of the circle.

You can easily imagine that users would like to mark on the map where they spotted a character in the park, and have that information communicated to other app users in the park. As well, the radius of the circle representing a character could change, depending on how long it has been since that character was last spotted.

You won’t go quite that far in this tutorial, but at the very least, you can load up some sample character coordinate data from a file and draw some circles on the map to simulate the location of those characters.

The MKCircle overlay is a very easy way to implement this functionality.

The resources for this tutorial contains the character location files (character-locations.zip), so make sure you unzip that file and add all the plist files inside to your project.

Each file is an array of a few coordinates where the user spotted characters.

Create a new Swift file under the Models group called Character. Open the new Character.swift and replace its contents with the following code:

import UIKit
import MapKit

class Character: MKCircle, MKOverlay {
  
  var name: String?
  var color: UIColor?
}

The new class that you just added conforms to the MKOverlay protocol, and defines two optional properties: name and color. And that’s it for this class — you don’t need anything more.

Now you need a method to add the character based on the data in the plist file. Open ParkMapViewController.swift and add the following method to the class:

func addCharacterLocation() {
  let batmanFilePath = NSBundle.mainBundle().pathForResource("BatmanLocations", ofType: "plist")
  let batmanLocations = NSArray(contentsOfFile: batmanFilePath!)
  let batmanPoint = CGPointFromString(batmanLocations![Int(rand()%4)] as! String)
  let batmanCenterCoordinate = CLLocationCoordinate2DMake(CLLocationDegrees(batmanPoint.x), CLLocationDegrees(batmanPoint.y))
  let batmanRadius = CLLocationDistance(max(5, Int(rand()%40)))
  let batman = Character(centerCoordinate:batmanCenterCoordinate, radius:batmanRadius)
  batman.color = UIColor.blueColor()
  
  let tazFilePath = NSBundle.mainBundle().pathForResource("TazLocations", ofType: "plist")
  let tazLocations = NSArray(contentsOfFile: tazFilePath!)
  let tazPoint = CGPointFromString(tazLocations![Int(rand()%4)] as! String)
  let tazCenterCoordinate = CLLocationCoordinate2DMake(CLLocationDegrees(tazPoint.x), CLLocationDegrees(tazPoint.y))
  let tazRadius = CLLocationDistance(max(5, Int(rand()%40)))
  let taz = Character(centerCoordinate:tazCenterCoordinate, radius:tazRadius)
  taz.color = UIColor.orangeColor()
  
  let tweetyFilePath = NSBundle.mainBundle().pathForResource("TweetyBirdLocations", ofType: "plist")
  let tweetyLocations = NSArray(contentsOfFile: tweetyFilePath!)
  let tweetyPoint = CGPointFromString(tweetyLocations![Int(rand()%4)] as! String)
  let tweetyCenterCoordinate = CLLocationCoordinate2DMake(CLLocationDegrees(tweetyPoint.x), CLLocationDegrees(tweetyPoint.y))
  let tweetyRadius = CLLocationDistance(max(5, Int(rand()%40)))
  let tweety = Character(centerCoordinate:tweetyCenterCoordinate, radius:tweetyRadius)
  tweety.color = UIColor.yellowColor()
  
  mapView.addOverlay(batman)
  mapView.addOverlay(taz)
  mapView.addOverlay(tweety)
}

The method above performs pretty much performs the same operations for each character. First, it reads in the data from the plist file and selects a random location from the four locations in the file. Next, it creates an instance of Character at the previously chosen random location, and sets the radius to a random value to simulate the time variance.

Finally, it assigns each character a color and adds it to the map as an overlay.

You’re almost done — can you recall what the last few steps should be?

Right — you still need to provide the map view with an MKOverlayView, which is done through the delegate method.

Update the delegate method in ParkMapViewController.swift to reflect the following:

func mapView(mapView: MKMapView!, rendererForOverlay overlay: MKOverlay!) -> MKOverlayRenderer! {
  if overlay is ParkMapOverlay {
    let magicMountainImage = UIImage(named: "overlay_park")
    let overlayView = ParkMapOverlayView(overlay: overlay, overlayImage: magicMountainImage!)
    
    return overlayView
  } else if overlay is MKPolyline {
    let lineView = MKPolylineRenderer(overlay: overlay)
    lineView.strokeColor = UIColor.greenColor()
    
    return lineView
  } else if overlay is MKPolygon {
    let polygonView = MKPolygonRenderer(overlay: overlay)
    polygonView.strokeColor = UIColor.magentaColor()
    
    return polygonView
  } else if overlay is Character {
    let circleView = MKCircleRenderer(overlay: overlay)
    circleView.strokeColor = (overlay as! Character).color
    
    return circleView
  }
  
  return nil
}

And finally, update loadSelectedOptions() to give the user an option to turn the character locations on or off:

func loadSelectedOptions() {
  mapView.removeAnnotations(mapView.annotations)
  mapView.removeOverlays(mapView.overlays)
    
  for option in selectedOptions {
    switch (option) {
      case .MapOverlay:
        addOverlay()
      case .MapPins:
        addAttractionPins()
      case .MapRoute:
        addRoute()
      case .MapBoundary:
        addBoundary()
      case .MapCharacterLocation:
        addCharacterLocation()
    }
  }
}

Build and run the app, and turn on the character overlay to see where everyone is hiding out!

Where to Go From Here?

Congratulations – you’ve worked with some of the most important functionality that MapKit provides. With a few basic functions, you’ve implemented a full-blown and practical mapping application complete with annotations, satellite view, and custom overlays!

Here’s the final example project that you developed in the tutorial.

There are many different ways to generate overlays that range from very easy, to the very complex. The approach in this tutorial that was taken for the overlay_park image provided in this tutorial was the easy — yet tedious — route.

To generate the overlay, you can start with a screenshot of the satellite view of the park. Then in your graphics tool of choice, just draw the roller coasters, building locations, trees, parking lot, and other details onto a new layer. If you know the latitude and longitude of the four corners of your starter screenshot, you can also calculate the coordinates for the park’s property list which the app uses to position the overlay on the map view.

There are much more advanced — and perhaps more efficient — methods to create overlays. A few alternate methods are to use KML files, MapBox tiles, or other 3rd party provided resources.

This tutorial didn’t delve into these overlay types in order to remain focused on the task of demonstrating the MapKit framework and APIs. But if you are serious about developing mapping apps, then you would do well to investigate these other options, and discover how to hook them into your apps!

I hope you enjoyed this tutorial, and I hope to see you use MapKit overlays in your own apps. If you have any questions or comments, please join the forum discussion below!

Niv Yahel

My name is Niv Yahel and I'm a senior software engineer at Opencare. I'm extremely passionate about coding and even more so about helping others improve. When I'm not working, I can be found following more tutorials, reading books, playing video games, or playing guitar.

Other Items of Interest

Save time.
Learn more with our video courses.

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 20 total!

Swift Team

... 15 total!

iOS Team

... 43 total!

Android Team

... 14 total!

macOS Team

... 11 total!

Unity Team

... 11 total!

Articles Team

... 12 total!

Resident Authors Team

... 16 total!