MapKit Tutorial: Overlay Views

In this MapKit Overlay tutorial, you’ll learn how to draw images and lines over a native iOS map to make it more interactive for your users. By Rony Rozen.

Leave a rating/review
Download materials
Save for later
Share
Update note: Rony Rozen updated this tutorial for Xcode 11 and Swift 5. Chris Wagner and Owen Brown wrote the previous versions of this tutorial.

While MapKit makes it easy to add a map to your app, that alone isn’t very engaging. Fortunately, you can use custom overlay views to make more appealing maps.

In this MapKit tutorial, you’ll create an app that showcases Six Flags Magic Mountain. By the time you’re done, you’ll have an interactive park map that shows attraction locations, ride routes and character locations. This app is for all you fast-ride thrill seekers out there. ;]

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial. Then, open the project in Xcode.

The starter project includes the map you’ll work with and buttons to toggle the different types of overlays on and off.

Build and run. You’ll see something like this:

ParkView starter project

Note: If you feel like you need a refresher on how to add a map to your app, or want to dive deeper on the basics of working with MapKit, visit MapKit Tutorial: Getting Started.

Once you feel ready, dive right into overlay views.

All About Overlay Views

Before you start creating overlay views, you need to understand two key classes: MKOverlay and MKOverlayRenderer.

MKOverlay tells MapKit where you want it to draw the overlays. There are three steps for using this class:

  1. First, create your custom class that implements the MKOverlay protocol, which has two required properties: coordinate and boundingMapRect. These properties define where the overlay resides on the map and its size.
  2. Then, create an instance of your class for each area where you want to display an overlay. In this app, for example, you’ll create an instance for a roller coaster overlay and another for a restaurant overlay.
  3. Finally, add the overlays to your map view.

At this point, the map knows where it’s supposed to display the overlays. But it doesn’t know what to display in each region.

This is where MKOverlayRenderer comes in. Subclassing it lets you set up what you want to display in each spot.

For example, in this app, you’ll draw an image of the roller coaster or restaurant. MapKit expects to present a MKMapView object, and this class defines the drawing infrastructure used by the map view.

Look at the starter project. In ContentView.swift, you’ll see a delegate method that lets you return an overlay view:

func mapView(
  _ mapView: MKMapView, 
  rendererFor overlay: MKOverlay
) -> MKOverlayRenderer

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

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

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

Adding Your Information to the Map

Currently, the map doesn’t provide enough information about the park. Your task is to create an object that represents an overlay for the entire park.

First, select the Overlays group and create a new Swift file named ParkMapOverlay.swift. Then replace its contents with:

import MapKit

class ParkMapOverlay: NSObject, MKOverlay {
  let coordinate: CLLocationCoordinate2D
  let boundingMapRect: MKMapRect
  
  init(park: Park) {
    boundingMapRect = park.overlayBoundingMapRect
    coordinate = park.midCoordinate
  }
}

Conforming to MKOverlay forces you to inherit from NSObject. The initializer takes the properties from the passed Park object, which is already in the starter project, and sets them to the corresponding MKOverlay properties.

Next, you need to create a MKOverlayRenderer that knows how to draw this overlay.

Create a new Swift file in the Overlays group called ParkMapOverlayView.swift. Replace its contents with:

import MapKit

class ParkMapOverlayView: MKOverlayRenderer {
  let overlayImage: UIImage
  
  // 1
  init(overlay: MKOverlay, overlayImage: UIImage) {
    self.overlayImage = overlayImage
    super.init(overlay: overlay)
  }
  
  // 2
  override func draw(
    _ mapRect: MKMapRect, 
    zoomScale: MKZoomScale, 
    in context: CGContext
  ) {
    guard let imageReference = overlayImage.cgImage else { return }
    
    let rect = self.rect(for: overlay.boundingMapRect)
    context.scaleBy(x: 1.0, y: -1.0)
    context.translateBy(x: 0.0, y: -rect.size.height)
    context.draw(imageReference, in: rect)
  }
}

Here’s a breakdown of what you added:

  1. init(overlay:overlayImage:) overrides the base method init(overlay:) by providing a second argument.
  2. draw(_:zoomScale:in:) is the real meat of this class. It defines how MapKit should render this view when given a specific MKMapRect, MKZoomScale and the CGContext of the graphic context, with the intent to draw the overlay image onto the context at the appropriate scale.
Note: The details of Core Graphics drawing are outside the scope of this tutorial. However, you can see the code above uses the passed MKMapRect to get a CGRect in which to draw the image in the provided context. To learn more about Core Graphics, check out the Core Graphics tutorial series.

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

Creating Your First Map Overlay

In ContentView.swift, find addOverlay() and change its TODO content to:

let overlay = ParkMapOverlay(park: park)
mapView.addOverlay(overlay)

This method adds an ParkMapOverlay to the map view.

Take a look at updateMapOverlayViews(). You’ll see when a user taps the button in the navigation bar to show the map overlay, addOverlay() is called. Now that you’ve added the necessary code, the overlay displays.

Notice that updateMapOverlayViews() also removes any annotations and overlays that may be present so you don’t end up with duplicate renderings. This is not necessarily efficient, but it’s a simple approach to clear previous items from the map.

The last step standing between you and seeing your newly implemented overlay on the map is mapView(_:rendererFor:), mentioned earlier. Replace its current TODO implementation with:

if overlay is ParkMapOverlay {
  return ParkMapOverlayView(
    overlay: overlay, 
    overlayImage: UIImage(imageLiteralResourceName: "overlay_park"))
}

When MapKit determines an MKOverlay is in view, it calls this delegate method to obtain a renderer.

Here, you check if the overlay is of class type ParkMapOverlay. If so, you load the overlay image, create a ParkMapOverlayView instance with the overlay image and return this instance to the caller.

There’s one little piece missing, though: Where does that suspicious little overlay_park image come from? It’s a PNG to overlay the map with the defined park’s boundary. The overlay_park image, found in Assets.xcassets, looks like this:

mapkit

Build and run, enable the :Overlay: option at the top of the screen and voilà! Here’s the park overlay drawn on top of your map:

ParkView with park overlay

Zoom in, zoom out, and move around. The overlay scales and moves as you would expect. Cool!