Advanced MapKit Tutorial: Custom MapKit Tiles

In this custom MapKit tiles tutorial, you’ll learn to modify the default MapKit tiles by adding cool, custom tiles to an adventure game. By Adam Rush.

Leave a rating/review
Download materials
Save for later
Share
Update note: Adam Rush updated this tutorial to Xcode 11, iOS 13 and Swift 5. Michael Katz wrote the original.

Maps are ubiquitous in modern apps. They provide locations of nearby points of interest, help users navigate a town or park, find nearby friends, track progress on a journey or provide context for an augmented reality game.

Unfortunately, this means most maps look the same from app to app. Booooooring!

This tutorial covers how to include hand-drawn maps in your apps, instead of programmatically-generated maps, as Pokémon GO uses. You’ll learn how to:

  • Replace existing MapKit tiles with a different set of tiles.
  • Create your own tiles to show on the map.
  • Add custom overlays to your map.

You’ll learn this by building a location-based adventure game. Taking a walk down fantasy Central Park, you’ll encounter formidable beasts that you’ll defeat on your way to glory!

Hand-drawing a map takes significant effort. Given the size of the planet, it’s only practical for a well-defined, geographically small area. If you have a well-defined area in mind for your map, a custom map can add a ton of sizzle to your app.

To create custom MapKit tiles, you’ll first have to know how to show a map. To get familiar with MapKit, check out MapKit Tutorial: Getting Started
or the video course MapKit and Core Location.

Getting Started

Start by downloading the project materials using the Download Materials button at the top or bottom of this tutorial. Open the starter project and check out the project files.

MapQuest is the start of a fun adventure game. The hero runs around Central Park, NYC in real life, but embarks on adventures, fights monsters, and collects treasure in an alternate reality. It has a cute, childish design to make players feel comfortable and to indicate the game isn’t that serious.

The game has several Points of Interest that define locations where the player can interact with the game. These can be quests, monsters, stores or other game elements. Entering a 10-meter zone around a Point of Interest starts the encounter. For the sake of this tutorial, the game-play isn’t as important as learning how to render the map.

There are two heavy-lifting files in the project:

  • MapViewController.swift: Manages the map view and handles user interaction logic and state changes.
  • Game.swift: Contains the game logic and manages the coordinates of some game objects.

The main view of the game is an MKMapView. MapKit uses tiles at various zoom levels to fill its view and provide information about geographic features, roads, etc.

The map view can display either a traditional road map or satellite imagery. This is helpful for navigating around a city, but useless for imagining you’re adventuring around a medieval world. However, MapKit lets you supply your own map art to customize the information it presents.

A map view is made up of many tiles that load dynamically as you pan around the view. The tiles are 256 by 256 pixels and are arranged in a grid that corresponds to a Mercator map projection.

To see the map in action, build and run the app.

Initial view of the MapQuest app covered by this custom MapKit tiles tutorial

Wow! What a pretty town. The game’s primary interface is location, which means there’s nothing to see or do without visiting Central Park. Don’t worry, though, you won’t need to buy a plane ticket just yet.

Testing Location

Unlike other tutorials, MapQuest is a functional app right out of the gate! But, unless you live in New York City, you can’t do much with the app. Fortunately, Xcode comes with at least two ways of handling this problem.

Simulating a Location

With the app still running in the iPhone Simulator, set the user’s location by going to Features ▸ Location ▸ Custom Location… and setting the Latitude to 40.767769 and Longitude to -73.971870.

This activates the blue user location dot and focuses the map on the Central Park Zoo. A wild goblin lives here. You’ll fight it, then collect its treasure.

A Wild Goblin appeared!

After beating up the helpless goblin, the app will place you in the zoo. Note the blue dot.

Hanging around at the zoo

Simulating an Adventure

A static location is useful for testing many location-based apps. However, this game requires visiting multiple locations as part of the adventure. The simulator can simulate changing locations for a run, a bike ride and a drive. These pre-included trips are for Cupertino, but MapQuest only has encounters in New York.

Occasions such as these call for simulating location with a GPX, or GPS Exchange Format, file. This file specifies waypoints and the simulator will interpolate a route between them.

Creating this file is outside the scope of this tutorial, but the sample project includes a test GPX file for you.

Open the scheme editor in Xcode by selecting Product ▸ Scheme ▸ Edit Scheme….

Select Run in the left pane, then the Options tab on the right. In the Core Location section, click the checkbox for Allow Location Simulation. In the Default Location drop-down, choose Game Test.

Editing the scheme with location changes

Now, the app will simulate moving between the waypoints specified in Game Test.gpx.

Build and run.

Hanging around at the zoo

The simulator will have your character walk from the 5th Avenue subway to the Central Park Zoo, where you’ll have to fight the goblin again. After that, it’s on to your favorite fruit company’s flagship store to buy an upgraded sword. Once you’ve completed the loop, the adventure will start over.

Now that the hero is walking down the map, you can get started adding custom MapKit tiles.

Replacing the Tiles With OpenStreetMap

OpenStreetMap is a community-supported open database of map data. You can use that data to generate map tiles like Apple Maps uses. The OpenStreetMap community provides more than basic road maps, they also offer specialized maps for topography, biking and artistic rendering.

Note: The OpenStreetMap tile policy has strict requirements about data usage, attribution and API access. Check for compliance before using the tiles in a production app.

Creating a New Overlay

To replace the map tiles, you need to use an MKTileOverlay to display new tiles on top of the default Apple Maps.

Open MapViewController.swift and replace setupTileRenderer() with the following:

private func setupTileRenderer() {
  // 1
  let template = "https://tile.openstreetmap.org/{z}/{x}/{y}.png"

  // 2
  let overlay = MKTileOverlay(urlTemplate: template)

  // 3
  overlay.canReplaceMapContent = true

  // 4
  mapView.addOverlay(overlay, level: .aboveLabels)

  //5
  tileRenderer = MKTileOverlayRenderer(tileOverlay: overlay)
}

By default, MKTileOverlay supports loading tiles by using a URL that’s templated to take a tile path.

Here’s what the code above does:

  1. First, you declare a URL template to fetch a tile from OpenStreetMap’s API. You replace the {x}, {y}, and {z} at runtime with the individual tile’s coordinates. How much the user has zoomed in on the map determines the z-coordinate, or zoom level coordinate. The x and y are the index of the tile for the section of the Earth you’re displaying. You need to supply a tile for each x and y coordinate for every zoom level you support.
  2. Next, you create the overlay.
  3. Then, you indicate that the tiles are opaque and that they should replace the default map tiles.
  4. You add the overlay to the mapView. Custom MapKit tiles can be either above the roads or above the labels (like road and place names). OpenStreetMap tiles come pre-labeled, so they should go above Apple’s labels.
  5. Finally, you create a tile renderer, which handles drawing the tiles.

Before the tiles can appear, you have to set up the tile renderer with MKMapView. So your next step is to add the following line to the bottom of viewDidLoad():

mapView.delegate = self

This sets the MapViewController to be the delegate of its mapView.

Next, in the MKMapViewDelegate extension, add the following method:

func mapView(
  _ mapView: MKMapView, 
  rendererFor overlay: MKOverlay
) -> MKOverlayRenderer {
  return tileRenderer
}

An overlay renderer tells the map view how to draw an overlay. The tile renderer is a special subclass for loading and drawing map tiles.

That’s it! Build and run to see how OpenStreetMap replaces the standard Apple map. But, how can this work with only a few lines of code?

Map now uses OpenStreetMap

At this point, you can really see the difference between the open-source maps and Apple Maps!