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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Indoor Maps on iOS: Advanced MapKit
25 mins
- Getting Started
- Understanding Indoor Maps
- What Is GeoJSON?
- What Is IMDF?
- Using the GeoJSON Format
- Modeling GeoJSON
- Decoding GeoJSON With MKGeoJSONDecoder
- Decoding the Archive
- Rendering Geometry on a Map
- Adding Overlays
- Adding Annotations
- Styling Map Geometry
- Selecting Levels
- Using Location With Indoor Maps
- Where To Go From Here?
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:
- Clear any existing annotations and overlays from the map and remove associated objects from the local cache.
- For the selected level, get associated features and store them inside the local arrays you just emptied.
- Create two arrays. The first contains the geometry from the units and occupants of the selected level. Then, using
compactMap(_:)
, you create an array ofMKOverlay
objects. - Call
addOverlays(_:)
andaddAnnotations(_:)
onmapView
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! :]
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. :]
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.
In the final section, you’ll learn how to use indoor locations to see where you are inside the office.