Learn how to add an overlay image using MapKit!
Update note: This tutorial was updated for Swift, iOS 8, and Xcode 6.1 by Niv Yahel. Previous updates by Cesare Rocchi, and original post by Chris Wagner.
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:
- 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.
- 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:
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:
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:
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:
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:
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:
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
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:
- 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.
- 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.
- 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!
Overlay Views with MapKit and Swift Tutorial is a post from: Ray Wenderlich
The post Overlay Views with MapKit and Swift Tutorial appeared first on Ray Wenderlich.