11

MapKit Tutorial: Overlay Views [FREE]

 3 years ago
source link: https://www.raywenderlich.com/9956648-mapkit-tutorial-overlay-views
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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:

Simulator-Screen-Shot-iPhone-11-Pro-2020-04-11-at-15.31.42-231x500.png

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 MKOverlayRenderer s 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:

overlay_park-351x500.png

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:

Simulator-Screen-Shot-iPhone-11-Pro-2020-04-11-at-16.10.20-231x500.png

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

Adding Annotations

If you’ve ever searched for a location in the native Maps app, you’ve seen those colored pins appear on the map. These are annotations created with MKAnnotationView . You can use annotations in your own app and use any image you want, not just pins!

Annotations are useful to help highlight specific points of interest to park visitors. They work similarly to MKOverlay and MKOverlayRenderer , but instead you’ll work with MKAnnotation and MKAnnotationView .

Writing Your First Annotation

First, create a new Swift file in the Annotations group called AttractionAnnotation.swift . Then, replace its contents with:

import MapKit

// 1
enum AttractionType: Int {
  case misc = 0
  case ride
  case food
  case firstAid
  
  func image() -> UIImage {
    switch self {
    case .misc:
      return UIImage(imageLiteralResourceName: "star")
    case .ride:
      return UIImage(imageLiteralResourceName: "ride")
    case .food:
      return UIImage(imageLiteralResourceName: "food")
    case .firstAid:
      return UIImage(imageLiteralResourceName: "firstaid")
    }
  }
}

// 2
class AttractionAnnotation: NSObject, MKAnnotation {
  // 3
  let coordinate: CLLocationCoordinate2D
  let title: String?
  let subtitle: String?
  let type: AttractionType
  
  // 4
  init(
    coordinate: CLLocationCoordinate2D,
    title: String,
    subtitle: String,
    type: AttractionType
  ) {
    self.coordinate = coordinate
    self.title = title
    self.subtitle = subtitle
    self.type = type
  }
}

Here’s what you added:

  1. AttractionType helps you categorize each attraction into a type. This enum lists four types of annotations: misc, rides, foods and first aid. There’s also a handy method to grab the correct annotation image.
  2. You create this class and make it conform to MKAnnotation .
  3. Much like MKOverlay , MKAnnotation has a required coordinate property. You define a handful of properties specific to this implementation.
  4. Finally, you define an initializer that lets you assign values to each of the properties.

Next, you’ll create a specific instance of MKAnnotationView to use for your annotations.

Associating a View With Your Annotation

First, create another Swift file called AttractionAnnotationView.swift in Annotations group. Then, replace its contents with the following snippet:

import MapKit

class AttractionAnnotationView: MKAnnotationView {
  // 1
  // Required for MKAnnotationView
  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
  }
  
  // 2
  override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
    super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
    guard 
      let attractionAnnotation = self.annotation as? AttractionAnnotation else { 
        return 
    }
    
    image = attractionAnnotation.type.image()
  }
}

Here’s a code breakdown:

  1. MKAnnotationView requires init(coder:) . Without its definition, an error prevents you from building and running the app. To avoid this, define it and call its superclass initializer.
  2. You also override init(annotation:reuseIdentifier:) and set a different annotation image based on the annotation’s type property.

Now that you’ve created the annotation and its associated view, you can start adding annotations to your map view!

Adding Annotations to the Map

To determine the location of each annotation, you’ll use the info in the MagicMountainAttractions.plist file, which you can find under the Park Information group. The plist file contains coordinate information and other details about the attractions at the park.

Go back to ContentView.swift and replace the TODO: implementation of addAttractionPins() with:

// 1
guard let attractions = Park.plist("MagicMountainAttractions") 
  as? [[String: String]] else { return }

// 2
for attraction in attractions {
  let coordinate = Park.parseCoord(dict: attraction, fieldName: "location")
  let title = attraction["name"] ?? ""
  let typeRawValue = Int(attraction["type"] ?? "0") ?? 0
  let type = AttractionType(rawValue: typeRawValue) ?? .misc
  let subtitle = attraction["subtitle"] ?? ""
  // 3
  let annotation = AttractionAnnotation(
    coordinate: coordinate, 
    title: title, 
    subtitle: subtitle, 
    type: type)
  mapView.addAnnotation(annotation)
}

Here’s a step by step breakdown:

  1. First, you read MagicMountainAttractions.plist and store it as an array of dictionaries.
  2. Then, you iterate over each dictionary in the array.
  3. For each entry, you create an instance of AttractionAnnotation with the point’s information and add it to the map view.

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 it can render them on itself.

Add the following method to the Coordinator class at the top of the file:

func mapView(
  _ mapView: MKMapView, 
  viewFor 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 . Since the property canShowCallout is set to true , a call-out appears when the user touches the annotation. Finally, the method returns the annotation view.

Build and run to see your annotations in action! Don’t forget to turn on the :Pins: option.

Simulator-Screen-Shot-iPhone-11-Pro-2020-04-11-at-16.28.38-231x500.png

The Attraction pins are looking rather sharp at this point! :]

So far, you’ve covered several complicated bits of MapKit, including overlays and annotations. But what if you need to use some drawing primitives, like lines and circles?

The MapKit framework also lets you draw directly onto a map view. MapKit provides MKPolyline , MKPolygon and MKCircle for this purpose. Time to try them out!

I Walk The Line: MKPolyline

If you’ve ever been to Magic Mountain, you know that the Goliath hypercoaster is an incredible ride. Some riders like to make a beeline for it as soon as they walk in the gate! :]

To help these riders out, you’ll plot a path from the entrance of the park straight 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.

To draw a polyline, you need a series of longitude and latitude coordinates in the order they should be plotted. EntranceToGoliathRoute.plist , again found in the Park Information folder, contains the path information.

Now, you need a way to read that plist file and create the route for the riders to follow.

First, open ContentView.swift and find addRoute() . Then, replace it’s current TODO implementation with:

guard let points = Park.plist("EntranceToGoliathRoute") as? [String] else { 
  return 
}
    
let cgPoints = points.map { NSCoder.cgPoint(for: $0) }
let coords = cgPoints.map { CLLocationCoordinate2D(
  latitude: CLLocationDegrees($0.x), 
  longitude: CLLocationDegrees($0.y))
}
let myPolyline = MKPolyline(coordinates: coords, count: coords.count)
    
mapView.addOverlay(myPolyline)

This method reads EntranceToGoliathRoute.plist and converts the individual coordinate strings to CLLocationCoordinate2D structures.

It’s remarkable how simple it is to implement a polyline: You simply create an array containing all of the points and pass it to MKPolyline ! It doesn’t get much easier than that.

Remember, updateMapOverlayViews() already calls addRoute() whenever the user toggles this option via the UI. All that’s left now is for you to update the delegate method so it returns the actual view you want to render on the map view.

Go back to mapView(_:rendererFor:) and add this else if clause to the existing condition:

else if overlay is MKPolyline {
  let lineView = MKPolylineRenderer(overlay: overlay)
  lineView.strokeColor = .green
  return lineView
}

The process of displaying the polyline view is very similar to previous overlay views. However, in this case, you don’t need to create any custom view objects. You simply use the provided MKPolyLineRenderer class and initialize a new instance with the overlay.

MKPolyLineRenderer also lets you change some of the polyline’s attributes. In this case, you’ve modified the stroke color to show as green.

Build and run your app. Enable the :Route: option and it’ll appear on the screen:

Simulator-Screen-Shot-iPhone-11-Pro-2020-04-11-at-16.43.00-231x500.png

Now Goliath fanatics can make it to the coaster in record time! :]

It would be nice to show the park patrons where the park boundaries are, since the park doesn’t actually occupy the entire space shown on the screen.

You could use MKPolyline to draw a shape around the park boundaries, but MapKit provides another class specifically designed to draw closed polygons: MKPolygon .

Don’t Fence Me In: MKPolygon

MKPolygon is similar to MKPolyline , except 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 shows the park boundaries. The park boundary coordinates are defined in MagicMountain.plist . Look at init(filename:) in Park.swift to see where the boundary points are read from the plist file.

Now, in ContentView.swift , replace the TODO implementation of addBoundary() with:

mapView.addOverlay(MKPolygon(
  coordinates: park.boundary, 
  count: park.boundary.count))

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 similar to what you did for MKPolyline .

Yup, that’s right. MKPolygon conforms to MKOverlay like MKPolyline does, so you need to update the delegate method again.

Go back to mapView(_:rendererFor:) and add this else if clause to the existing conditions:

else if overlay is MKPolygon {
  let polygonView = MKPolygonRenderer(overlay: overlay)
  polygonView.strokeColor = .magenta
  return polygonView
}

You create an MKOverlayView as an instance of MKPolygonRenderer and set the stroke color to magenta.

Run the app and enable the :Bound: option to see your new boundary in action. You may have to zoom out so the park boundaries can fit the simulator’s screen bounds.

Simulator-Screen-Shot-iPhone-11-Pro-2020-04-11-at-16.54.55-231x500.png

That takes care of polylines and polygons. The last drawing method to cover is drawing circles as an overlay, which you’ll do with MKCircle .

Circle in the Sand: MKCircle

MKCircle is also very similar to MKPolyline and MKPolygon , except it draws a circle when given the center coordinate point and a radius that determines the size of the circle.

Many park visitors like to visit with the characters. You can help them find the characters by marking where they were last spotted on the map with circles. The MKCircle overlay lets you do this easily.

The Park Information folder also contains the character location files. Each file is an array of a few coordinates where the user spotted characters.

First, create a new swift file under the Models group called Character.swift . Then replace its contents with the following code:

import MapKit

// 1
class Character: MKCircle {
  // 2
  private var name: String?
  var color: UIColor?
  
  // 3
  convenience init(filename: String, color: UIColor) {
    guard let points = Park.plist(filename) as? [String] else { 
      self.init()
      return
    }
    
    let cgPoints = points.map { NSCoder.cgPoint(for: $0) }
    let coords = cgPoints.map {
      CLLocationCoordinate2D(
        latitude: CLLocationDegrees($0.x), 
        longitude: CLLocationDegrees($0.y))
    }
    
    let randomCenter = coords[Int.random(in: 0...3)]
    let randomRadius = CLLocationDistance(Int.random(in: 5...39))
    
    self.init(center: randomCenter, radius: randomRadius)
    self.name = filename
    self.color = color
  }
}

Here’s what this code does:

  1. Character class conforms to MKCircle protocol.
  2. It defines two optional properties: name and color .
  3. The convenience initializer accepts a plist filename and color to draw the circle. Then it reads in the data from the plist file and selects a random location from the four locations in the file. Next, it chooses a random radius to simulate the time variance. The MKCircle returned is set and ready to be put on the map!

Now you need a method to add the characters. So, open ContentView.swift and replace the TODO implementation of addCharacterLocation() with:

mapView.addOverlay(Character(filename: "BatmanLocations", color: .blue))
mapView.addOverlay(Character(filename: "TazLocations", color: .orange))
mapView.addOverlay(Character(filename: "TweetyBirdLocations", color: .yellow))

This method performs pretty much the same operations for each character: It passes the plist filename for each one, decides on a color and adds it to the map as an overlay.

You’re almost done! Do you remember the last step?

Right! You need to give the map view a MKOverlayView by using the delegate method.

Go back to mapView(_:rendererFor:) and add this else if clause to the existing conditions:

else if let character = overlay as? Character {
  let circleView = MKCircleRenderer(overlay: character)
  circleView.strokeColor = character.color
  return circleView
}

Build and run the app, and enable the :Characters: option to see where everyone is hiding!

Uzqeiib.png!web

Where to Go From Here?

You can download the complete project by using the Download Materials button at the top or bottom of this tutorial.

Congratulations! You’ve worked with some of the most important capabilities that MapKit provides. With a few basic functions, you’ve implemented a full-blown and practical mapping app complete with annotations and custom overlays.

There are many ways to generate overlays that range from very easy to very complex. The approach taken in this tutorial for the overlay_park image was the easy, yet tedious, route.

There are much more advanced, and perhaps more efficient, methods to create overlays. For example, you could use KML tiles or other third party provided resources.

To learn more about MapKit, check out the other MapKit tutorials on our site, such as Advanced MapKit Tutorial: Custom Tiles .

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!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK