Indoor Maps on iOS: Advanced MapKit

In this MapKit tutorial, you’ll learn how to use Indoor Maps to map the inside of buildings, switch between different stories and find your location inside the building. By Alex Brown.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Adding Overlays

Open MapViewController.swift and scroll down to extension MapViewController: MKMapViewDelegate. You’ll notice that you already have some methods defined here.

MapKit calls mapView(_:rendererFor:) -> MKOverlayRenderer whenever it needs to draw an overlay on the map view. This method is responsible for passing the appropriate MKOverlayRenderer back to MapKit.

Right now, it’s not doing much, but you’ll change that next.

Replace the entire method with the following code:

func mapView(
  _ mapView: MKMapView, 
  rendererFor overlay: MKOverlay
) -> MKOverlayRenderer {
  guard
    let shape = overlay as? (MKShape & MKGeoJSONObject),
    let feature = currentLevelFeatures
      .first( where: { $0.geometry.contains( where: { $0 == shape }) })
    else { return MKOverlayRenderer(overlay: overlay) }

  let renderer: MKOverlayPathRenderer
  switch overlay {
  case is MKMultiPolygon:
    renderer = MKMultiPolygonRenderer(overlay: overlay)
  case is MKPolygon:
    renderer = MKPolygonRenderer(overlay: overlay)
  case is MKMultiPolyline:
    renderer = MKMultiPolylineRenderer(overlay: overlay)
  case is MKPolyline:
    renderer = MKPolylineRenderer(overlay: overlay)
  default:
    return MKOverlayRenderer(overlay: overlay)
  }

  feature.configure(overlayRenderer: renderer)

  return renderer
}

When addOverlays(_:) is called on the delegating MKMapView, this method is called. The overlay provided to addOverlays(_:) is passed in.

As you learned previously, MKOverlay is a protocol that concrete classes adopt to create basic shapes. So in the code above you check to see which type it is and create the appropriate MKOverlayRenderer subclass depending on the result.

Finally, before returning the renderer created, you call configure(overlayRenderer:) on the feature. This is a method in StylableFeature.

You’ll see exactly what StylableFeature is and does later in the tutorial.

But first up, you must learn how to add annotations to the map alongside overlays.

Adding Annotations

You add annotations in a similar way to overlays. MKMapViewDelegate has a delegate method, mapView(_:viewFor:) -> MKAnnotationView?, which is called each time addAnnotations(_:) is called on the delegating MKMapView.

Just as with overlays, this method isn’t called until you add an annotation to the map. Right now, that’s not happening — but it’s about time you changed that. :]

Add the following method below showDefaultMapRect():

private func showFeatures(for ordinal: Int) {
  guard venue != nil else {
    return
  }

  // 1
  currentLevelFeatures.removeAll()
  mapView.removeOverlays(currentLevelOverlays)
  mapView.removeAnnotations(currentLevelAnnotations)
  currentLevelAnnotations.removeAll()
  currentLevelOverlays.removeAll()

  // 2
  if let levels = venue?.levelsByOrdinal[ordinal] {
    for level in levels {
      currentLevelFeatures.append(level)
      currentLevelFeatures += level.units
      currentLevelFeatures += level.openings

      let occupants = level.units.flatMap { unit in
        unit.occupants
      }

      let amenities = level.units.flatMap { unit in
        unit.amenities
      }

      currentLevelAnnotations += occupants
      currentLevelAnnotations += amenities
    }
  }

  // 3
  let currentLevelGeometry = currentLevelFeatures.flatMap { 
    feature in
    feature.geometry
  }

  currentLevelOverlays = currentLevelGeometry.compactMap { 
    mkOverlay in
    mkOverlay as? MKOverlay
  }

  // 4
  mapView.addOverlays(currentLevelOverlays)
  mapView.addAnnotations(currentLevelAnnotations)
}

The main responsibility of this method is to tear down existing overlays and annotations and draw new ones in their place. It takes a single parameter, ordinal, which specifies which level in the venue to get the geometry for.

Here’s a breakdown:

  1. Clear any existing annotations and overlays from the map and remove associated objects from the local cache.
  2. For the selected level, get associated features and store them inside the local arrays you just emptied.
  3. Create two arrays. The first contains the geometry from the units and occupants of the selected level. Then, using compactMap(_:), you create an array of MKOverlay objects.
  4. Call addOverlays(_:) and addAnnotations(_:) on mapView to add the annotations and overlays to the map.

To see this code in action, at the bottom of viewDidLoad(), add:

showFeatures(for: 1)

Build and run. Try clicking the annotations on the map to look at the features present.

You’re a real cartographer now! :]

RazeWare office geometry drawn on a map

Now, your map is functional and shows the RazeWare office as you intended. However, it doesn’t look as nice as it should. In the next step, you’ll add your own style to the map.

Styling Map Geometry

You’ve handled a lot of the styling for the geometry already, but you still see two default map pins. You need to replace those with something more stylish.

In this section, you’ll learn what StyleableFeature does and how to use it to style the Occupant model type.

StylableFeature is a protocol provided by the sample project — not MapKit — that defines two methods which conforming types should adopt to provide custom styles for a feature.

Open IMDF/StylableFeatures.swift to view the contents.

protocol StylableFeature {
  var geometry: [MKShape & MKGeoJSONObject] { get }
  func configure(overlayRenderer: MKOverlayPathRenderer)
  func configure(annotationView: MKAnnotationView)
}

extension StylableFeature {
  func configure(overlayRenderer: MKOverlayPathRenderer) {}
  func configure(annotationView: MKAnnotationView) {}
}

You’ll notice that both configure methods have a default empty implementation because they’re not required. You’ll see why in a moment. The only other requirement is that a conforming object provide an array of geometry objects that inherit from MKShape and conform to MKGeoJSONObject.

Open IMDF/Models/Occupant.swift and add the following extension to the bottom of the file, after the closing brace:

extension Occupant {
  private enum StylableCategory: String {
    case play
    case office
  }
}

StylableCategory defines the two types used in occupant.geojson.

Open occupant.geojson and take a look at the category key for both objects. You’ll notice they contain play and office. These types are completely arbitrary and can be anything the author of the GeoJSON chooses. You can create as many of these categories as you like.

Continuing in Occupant.swift, add this next extension to the bottom of the file, after the closing brace:

extension Occupant: StylableFeature {
  func configure(annotationView: MKAnnotationView) {
    if let category = StylableCategory(rawValue: properties.category) {
      switch category {
      case .play:
        annotationView.backgroundColor = UIColor(named: "PlayFill")
      case .office:
        annotationView.backgroundColor = UIColor(named: "OfficeFill")
      }
    }

    annotationView.displayPriority = .defaultHigh
  }
}

Now, Occupant conforms to StylableFeature and implements configure(annotationView: MKAnnotationView). This method checks that category is a valid StylableCategory and returns a color based on the value. If one overlay collides with another, the overlay with the lowest displayPriority is hidden.

Build and run. You’ll see that those hideous default map pins have disappeared. In their place are two map points with custom colors. :]

RazeWare office geometry drawn on a map with custom map points

Selecting Levels

You’ve come across Level a few times now. It describes one level, or story, of a building and can have its own set of features. Take a shopping mall for example: They usually have multiple stories with different shops on each floor. Using Level, you could describe which shops are on each floor.

A segmented control is already in place for the interface, and it’s already connected to segmentedControlValueChanged(_ sender:). You also have the ability to redraw geometry based on a Level with showFeatures(for:). You’re on a roll! :]

Open MapViewController.swift and add the following code to the body of segmentedControlValueChanged(_ sender:):

showFeatures(for: sender.selectedSegmentIndex)

Build and run. When you change the level on the segmented control, the map overlays redraw the provided level.

Switching between Dungeon and First Floor levels on the indoor maps

In the final section, you’ll learn how to use indoor locations to see where you are inside the office.

Alex Brown

Contributors

Alex Brown

Author

David Sherline

Tech Editor

Julia Zinchenko

Illustrator

Morten Faarkrog

Final Pass Editor

Richard Critz

Team Lead

Cosmin Pupăză

Topics Master

Over 300 content creators. Join our team.