Chapter 20. Maps

Your app can imitate the Maps app, displaying a map interface and placing annotations and overlays on the map. The relevant classes are provided by the Map Kit framework. You’ll need to import MapKit. The classes used to describe locations in terms of latitude and longitude, whose names start with “CL,” come from the Core Location framework, but you won’t need to import it explicitly if you’re already importing the Map Kit framework.

Displaying a Map

A map is displayed through a UIView subclass, an MKMapView. You can instantiate an MKMapView from a nib or create one in code. A map has a type, which is one of the following (MKMapType):

  • .standard

  • .satellite

  • .hybrid

The area displayed on the map is its region, an MKCoordinateRegion. This is a struct comprising a location (its center, a CLLocationCoordinate2D), describing the latitude and longitude of the point at the center of the region, along with a span (an MKCoordinateSpan), describing the quantity of latitude and longitude embraced by the region and hence the scale of the map. Convenience functions help you construct an MKCoordinateRegion.

In this example, I’ll initialize the display of an MKMapView (self.map) to show a place where I like to go dirt biking (Figure 20-1):

let loc = CLLocationCoordinate2DMake(34.927752,-120.217608)
let span = MKCoordinateSpanMake(0.015, 0.015)
let reg = MKCoordinateRegionMake(loc, span)
self.map.region = reg
pios 3401
Figure 20-1. A map view showing a happy place

An MKCoordinateSpan is described in degrees of latitude and longitude. It may be, however, that what you know is the region’s proposed dimensions in meters. To convert, call MKCoordinateRegionMakeWithDistance. The ability to perform this conversion is important, because an MKMapView shows the world through a Mercator projection, where longitude lines are parallel and equidistant, and scale increases at higher latitudes.

I happen to know that the area I want to display is about 1200 meters on a side. Hence, this is another way of displaying roughly the same region:

let loc = CLLocationCoordinate2DMake(34.927752,-120.217608)
let reg = MKCoordinateRegionMakeWithDistance(loc, 1200, 1200)
self.map.region = reg

Yet another way of describing a map region is with an MKMapRect, a struct built up from MKMapPoint and MKMapSize. The earth has already been projected onto the map for us, and now we are describing a rectangle of that map, in terms of the units in which the map is drawn. The exact relationship between an MKMapPoint and the corresponding location coordinate is arbitrary and of no interest; what matters is that you can ask for the conversion, along with the ratio of points to meters (which will vary with latitude):

  • MKMapPointForCoordinate

  • MKCoordinateForMapPoint

  • MKMetersPerMapPointAtLatitude

  • MKMapPointsPerMeterAtLatitude

  • MKMetersBetweenMapPoints

To determine what the map view is showing in MKMapRect terms, use its visibleMapRect property. Thus, this is another way of displaying approximately the same region:

let loc = CLLocationCoordinate2DMake(34.927752,-120.217608)
let pt = MKMapPointForCoordinate(loc)
let w = MKMapPointsPerMeterAtLatitude(loc.latitude) * 1200
self.map.visibleMapRect = MKMapRectMake(pt.x - w/2.0, pt.y - w/2.0, w, w)

In none of those examples did I bother with the question of the actual dimensions of the map view itself. I simply threw a proposed region at the map view, and it decided how best to portray the corresponding area. Values you assign to the map’s region and visibleMapRect are unlikely to be the exact values the map adopts in any case; that’s because the map view will optimize for display without distorting the map’s scale. You can perform this same optimization in code by calling these methods:

  • regionThatFits(_:)

  • mapRectThatFits(_:)

  • mapRectThatFits(_:edgePadding:)

By default, the user can zoom and scroll the map with the usual gestures; you can turn this off by setting the map view’s isZoomEnabled and isScrollEnabled to false. Usually you will set them both to true or both to false. For further customization of an MKMapView’s response to touches, use a UIGestureRecognizer (Chapter 5).

You can change programmatically the region displayed, optionally with animation, by calling these methods:

  • setRegion(_:animated:)

  • setCenter(_:animated:)

  • setVisibleMapRect(_:animated:)

  • setVisibleMapRect(_:edgePadding:animated:)

The map view’s delegate (MKMapViewDelegate) is notified as the map loads and as the region changes (including changes triggered programmatically):

  • mapViewWillStartLoadingMap(_:)

  • mapViewDidFinishLoadingMap(_:)

  • mapViewDidFailLoadingMap(_:withError:)

  • mapView(_:regionWillChangeAnimated:)

  • mapView(_:regionDidChangeAnimated:)

You can also enable 3D viewing of the map (pitchEnabled), and there’s a large and powerful API putting control of 3D viewing in your hands. Discussion of 3D map viewing is beyond the scope of this chapter; an excellent WWDC 2013 video surveys the topic. Starting in iOS 9, there are 3D flyover map types .satelliteFlyover and .hybridFlyover; a WWDC 2015 video explains about these.

Also starting in iOS 9, an MKMapView has Bool properties showsCompass, showsScale, and showsTraffic.

Annotations

An annotation is a marker associated with a location on a map. To make an annotation appear on a map, two objects are needed:

The object attached to the MKMapView

The annotation itself is attached to the MKMapView. It consists of any instance whose class adopts the MKAnnotation protocol, which specifies a coordinate, a title, and a subtitle for the annotation. You might have reason to define your own class to handle this task, or you can use the simple built-in MKPointAnnotation class. The annotation’s coordinate is crucial; it says where on earth the annotation should be drawn. The title and subtitle are optional, to be displayed in a callout.

The object that draws the annotation

An annotation is drawn by an MKAnnotationView, a UIView subclass. This can be extremely simple. In fact, even a nil MKAnnotationView might be perfectly satisfactory: it draws a red pin. If red is not your favorite color, a built-in MKAnnotationView subclass, MKPinAnnotationView, displays a pin in red, green, or purple (conventionally designating destination points, starting points, and user-specified points, respectively) — or, starting in iOS 9, you are free to set an MKPinAnnotationView’s pin color (its pinTintColor property) to any UIColor.

If a pin is not your thing, you can provide your own UIImage as the MKAnnotationView’s image property. And for even more flexibility, you can take over the drawing of an MKAnnotationView by overriding draw(_:) in a subclass.

Not only does an annotation require two separate objects, but in fact those objects do not initially exist together. An annotation object has no pointer to the annotation view object that will draw it. Rather, it is up to you to supply the annotation view object in real time, on demand, in the MKMapView’s delegate. This architecture may sound confusing, but in fact it’s a very clever way of reducing the amount of resources needed at any given moment. An annotation itself is merely a lightweight object that a map can always possess; the corresponding annotation view is a heavyweight object that is needed only so long as that annotation’s coordinates are within the visible portion of the map.

Let’s add the simplest possible annotation to our map. The point where the annotation is to go has been stored in an instance property:

self.annloc = CLLocationCoordinate2DMake(34.923964,-120.219558)

We create the annotation, configure it, and add it to the MKMapView:

let ann = MKPointAnnotation()
ann.coordinate = self.annloc
ann.title = "Park here"
ann.subtitle = "Fun awaits down the road!"
self.map.addAnnotation(ann)

That code is sufficient to produce Figure 20-2. I didn’t implement any MKMapView delegate methods, so the MKAnnotationView is nil. But a nil MKAnnotationView, as I’ve already said, produces a red pin. I’ve also tapped the annotation, to display its callout, containing the annotation’s title and subtitle.

pios 3402
Figure 20-2. A simple annotation

Custom Annotation View

The location marked by our annotation is the starting point of a suggested dirt bike ride, so by convention the pin should be green. We can easily create a green pin using MKPinAnnotationView, which has a pinTintColor property. To supply the annotation view, we must give the map view a delegate (MKMapViewDelegate) and implement mapView(_:viewFor:). The second parameter is the MKAnnotation for which we are to supply a view.

The structure of mapView(_:viewFor:) is rather similar to the structure of tableView(_:cellForRowAt:) (Chapter 8), which is not surprising, considering that they both do the same sort of thing. Recall that the goal of tableView(_:cellForRowAt:) is to allow the table view to reuse cells, so that at any given moment only as many cells are needed as are visible in the table view, regardless of how many rows the table as a whole may consist of. The same thing holds for a map and its annotation views. The map may have a huge number of annotations, but it needs to display annotation views for only those annotations that are within its current region. Any extra annotation views that have been scrolled out of view can thus be reused and are held for us by the map view in a cache for exactly this purpose.

So, in mapView(_:viewFor:), we start by calling dequeueReusableAnnotationView(withIdentifier:) to see whether there’s an already existing annotation view that’s not currently being displayed and that we might be able to reuse. If there isn’t, we create one, attaching to it an appropriate reuse identifier.

Here’s our implementation of mapView(_:viewFor:). Observe that in creating our green pin, we explicitly set its canShowCallout to true, as this is not the default:

func mapView(_ mapView: MKMapView,
    viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        var v : MKAnnotationView! = nil
        if let t = annotation.title, t == "Park here" { 1
            let ident = "greenPin" 2
            v = mapView.dequeueReusableAnnotationView(withIdentifier:ident)
            if v == nil {
                v = MKPinAnnotationView(annotation:annotation,
                    reuseIdentifier:ident)
                (v as! MKPinAnnotationView).pinTintColor =
                    MKPinAnnotationView.greenPinColor() // or any UIColor
                v.canShowCallout = true
            }
            v.annotation = annotation 3
        }
        return v
}

The structure of this implementation of mapView(_:viewFor:) is typical (though it seems pointlessly elaborate when we have only one annotation in our map):

1

We might have more than one reusable type of annotation view, so we must somehow distinguish the possible cases, based on something about the incoming annotation. Here, I use the annotation’s title as a distinguishing mark; later in this chapter, I’ll suggest a much better approach.

2

For each reusable type, we proceed much as with table view cells. We have an identifier that categorizes this sort of reusable view. We try to dequeue an unused annotation view of the appropriate type. If we can’t, we’ll get nil; in that case, we create an MKAnnotationView and configure it (compare Example 8-1).

3

Even if we can dequeue an unused annotation view, and even if we have no other configuration to perform, we must associate the annotation view with the incoming annotation by assigning the annotation to this annotation view’s annotation property. Forgetting to do this is a common beginner mistake.

MKPinAnnotationView has one more option of which we might avail ourselves: when it draws the annotation view (the pin), it can animate it into place, dropping it in the manner familiar from the Maps app. All we have to do is add one line of code:

(v as! MKPinAnnotationView).animatesDrop = true

Now let’s go further. Instead of a pin, we’ll substitute our own artwork. I’ll revise the code at the heart of my mapView(_:viewFor:) implementation, such that instead of creating an MKPinAnnotationView, I create an instance of its superclass, MKAnnotationView, and give it a custom image showing a dirt bike. The image is too large, so I shrink the view’s bounds before returning it; I also move the view up a bit, so that the bottom of the image is at the coordinates on the map (Figure 20-3):

func mapView(_ mapView: MKMapView,
    viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        var v : MKAnnotationView! = nil
        if let t = annotation.title, t == "Park here" {
            let ident = "bike"
            v = mapView.dequeueReusableAnnotationView(withIdentifier:ident)
            if v == nil {
                v = MKAnnotationView(annotation:annotation,
                    reuseIdentifier:ident)
                v.image = UIImage(named:"clipartdirtbike.gif")
                v.bounds.size.height /= 3.0
                v.bounds.size.width /= 3.0
                v.centerOffset = CGPoint(0,-20)
                v.canShowCallout = true
            }
            v.annotation = annotation
        }
        return v
}
pios 3403
Figure 20-3. A custom annotation image

For more flexibility, we can create our own MKAnnotationView subclass and endow it with the ability to draw itself. At a minimum, such a subclass should override the initializer and assign itself a frame, and should implement draw(_:). Here’s the implementation for a class MyAnnotationView that draws a dirt bike:

class MyAnnotationView : MKAnnotationView {
    override init(annotation:MKAnnotation?, reuseIdentifier:String?) {
        super.init(annotation: annotation,
            reuseIdentifier: reuseIdentifier)
        let im = UIImage(named:"clipartdirtbike.gif")!
        self.frame = CGRect(0, 0,
            im.size.width / 3.0 + 5, im.size.height / 3.0 + 5)
        self.centerOffset = CGPoint(0,-20)
        self.isOpaque = false
    }
    required init(coder: NSCoder) {
        fatalError("NSCoding not supported")
    }
    override func draw(_ rect: CGRect) {
        let im = UIImage(named:"clipartdirtbike.gif")!
        im.draw(in:self.bounds.insetBy(dx: 5, dy: 5))
    }
}

The corresponding implementation of mapView(_:viewFor:) now has much less work to do:

func mapView(_ mapView: MKMapView,
    viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        var v : MKAnnotationView! = nil
        if let t = annotation.title, t == "Park here" {
            let ident = "bike"
            v = mapView.dequeueReusableAnnotationView(withIdentifier:ident)
            if v == nil {
                v = MyAnnotationView(annotation:annotation,
                    reuseIdentifier:ident)
                v.canShowCallout = true
            }
            v.annotation = annotation
        }
        return v
}

Custom Annotation Class

For ultimate flexibility, we can provide our own annotation class as well. A minimal annotation class will look like this:

class MyAnnotation : NSObject, MKAnnotation {
    dynamic var coordinate : CLLocationCoordinate2D
    var title: String?
    var subtitle: String?
    init(location coord:CLLocationCoordinate2D) {
        self.coordinate = coord
        super.init()
    }
}

Now when we create our annotation and add it to our map, our code looks like this:

let ann = MyAnnotation(location:self.annloc)
ann.title = "Park here"
ann.subtitle = "Fun awaits down the road!"
self.map.addAnnotation(ann)

A major advantage of this change appears in our implementation of mapView(_:viewFor:), where we test for the annotation type. Formerly, it wasn’t easy to distinguish those annotations that needed to be drawn as a dirt bike; we were rather artificially examining the title:

if let t = annotation.title, t == "Park here" {

Now, however, we can just look at the class:

if annotation is MyAnnotation {

A further advantage of supplying our own annotation class is that this approach gives our implementation room to grow. For example, at the moment, every MyAnnotation is drawn as a bike, but we could now add another property to MyAnnotation that tells us what drawing to use. We could also give MyAnnotation further properties saying such things as which way the bike should face, what angle it should be drawn at, and so on. Each MyAnnotationView instance will end up with a reference to the corresponding MyAnnotation instance (as its annotation property), so it would be able to read those MyAnnotation properties and draw itself appropriately.

Other Annotation Features

To add our own animation to an annotation view as it appears on the map, analogous to the built-in MKPinAnnotationView pin-drop animation, we implement the map view delegate method mapView(_:didAdd:), which hands us an array of MKAnnotationViews. The key fact here is that at the moment this method is called, the annotation views have been added but the redraw moment has not yet arrived (Chapter 4). So if we animate a view, that animation will be performed at the moment the view appears onscreen. Here, I’ll animate the opacity of our annotation view so that it fades in, while growing the view from a point to its full size; I identify the view type through its reuseIdentifier:

func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) {
    for aView in views {
        if aView.reuseIdentifier == "bike" {
            aView.transform = CGAffineTransform(scaleX: 0, y: 0)
            aView.alpha = 0
            UIView.animate(withDuration:0.8) {
                aView.alpha = 1
                aView.transform = .identity
            }
        }
    }
}

The callout is visible in Figures 20-2 and 20-3, because before taking the screenshot, I tapped on the annotation, thus selecting it. MKMapView has methods allowing annotations to be selected or deselected programmatically, thus (by default) causing their callouts to appear or disappear. The delegate has methods notifying you when the user selects or deselects an annotation, and you are free to override your custom MKAnnotationView’s setSelected(_:animated:) if you want to change what happens when the user taps an annotation. For example, you could show and hide a custom view instead of, or in addition to, the built-in callout.

A callout can contain left and right accessory views; these are the MKAnnotationView’s leftCalloutAccessoryView and rightCalloutAccessoryView. They are UIViews, and should be small (less than 32 pixels in height). Starting in iOS 9, there is also a detailCalloutAccessoryView which replaces the subtitle; for example, you could supply a multiline label with smaller text, something that was quite difficult in earlier system versions. The map view’s tintColor (see Chapter 12) affects such accessory view elements as template images and button titles. You can respond to taps on these views as you would any view or control.

An MKAnnotationView can optionally be draggable by the user; set its draggable property to true and implement the map view delegate’s mapView(_:annotationView:didChange:fromOldState:). A minimal implementation must update the MKAnnotationView’s dragState, like this:

func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView,
    didChange newState: MKAnnotationViewDragState,
    fromOldState oldState: MKAnnotationViewDragState) {
        switch newState {
        case .starting:
            view.dragState = .dragging
        case .ending, .canceling:
            view.dragState = .none
        default: break
        }
}

(You can also customize changes to the appearance of the view as it is dragged, by implementing your annotation view class’s setDragState(_:animated:) method.) If you’re using a custom annotation class, its coordinate property must also be settable. In our custom annotation class, MyAnnotation, the coordinate property is settable; it is explicitly declared as a read-write property (var), as opposed to the coordinate property in the MKAnnotation protocol which is read-only.

Certain annotation properties and annotation view properties are automatically animatable through view animation, provided you’ve implemented them in a KVO compliant way. In MyAnnotation, for example, the coordinate property is KVO compliant (because we declared it dynamic); therefore, we are able to animate shifting the annotation’s position:

UIView.animate(withDuration:0.25) {
    var loc = ann.coordinate
    loc.latitude = loc.latitude + 0.0005
    loc.longitude = loc.longitude + 0.001
    ann.coordinate = loc
}

MKMapView has extensive support for adding and removing annotations. Also, given a bunch of annotations, you can ask your MKMapView to zoom in such a way that all of them are showing (showAnnotations(_:animated:)).

Annotation views don’t change size as the map is zoomed in and out, so if there are several annotations and they are brought close together by the user zooming out, the display can become crowded. Moreover, if too many annotations are being drawn simultaneously in a map view, scroll and zoom performance can degrade. The only way to prevent this is to respond to changes in the map’s visible region — for example, in the delegate method mapView(_:regionDidChangeAnimated:) — by removing and adding annotations dynamically. This is a tricky problem; MKMapView’s annotations(in:) efficiently lists the annotations within a given MKMapRect, but deciding which ones to eliminate or restore, and when, is still up to you.

Overlays

An overlay differs from an annotation in being drawn entirely with respect to points on the surface of the earth. Thus, whereas an annotation’s size is always the same, an overlay’s size is tied to the zoom of the map view.

Overlays are implemented much like annotations. You provide an object that adopts the MKOverlay protocol (which itself conforms to the MKAnnotation protocol) and add it to the map view. When the map view delegate method mapView(_:rendererFor:) is called, you provide an MKOverlayRenderer and hand it the overlay object; the overlay renderer then draws the overlay on demand. As with annotations, this architecture means that the overlay itself is a lightweight object, and the overlay is drawn only if the part of the earth that the overlay covers is actually being displayed in the map view. An MKOverlayRenderer has no reuse identifier; it isn’t a view, but rather a drawing engine that draws into a CGContext supplied by the map view.

Some built-in MKShape subclasses adopt the MKOverlay protocol: MKCircle, MKPolygon, and MKPolyline. In parallel to those, MKOverlayRenderer has built-in subclasses MKCircleRenderer, MKPolygonRenderer, and MKPolylineRenderer, ready to draw the corresponding shapes. Thus, as with annotations, you can base your overlay entirely on the power of existing classes.

In this example, I’ll use MKPolygonRenderer to draw an overlay triangle pointing up the road from the parking place annotated in our earlier examples (Figure 20-4). We add the MKPolygon as an overlay to our map view, and supply its corresponding MKPolygonRenderer in our implementation of mapView(_:rendererFor:). First, the MKPolygon overlay:

pios 3404
Figure 20-4. An overlay
let lat = self.annloc.latitude
let metersPerPoint = MKMetersPerMapPointAtLatitude(lat)
var c = MKMapPointForCoordinate(self.annloc)
c.x += 150/metersPerPoint
c.y -= 50/metersPerPoint
var p1 = MKMapPointMake(c.x, c.y)
p1.y -= 100/metersPerPoint
var p2 = MKMapPointMake(c.x, c.y)
p2.x += 100/metersPerPoint
var p3 = MKMapPointMake(c.x, c.y)
p3.x += 300/metersPerPoint
p3.y -= 400/metersPerPoint
var pts = [
    p1, p2, p3
]
let tri = MKPolygon(points:&pts, count:3)
self.map.add(tri)

Second, the delegate method, where we provide the MKPolygonRenderer:

func mapView(_ mapView: MKMapView,
    rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
        if let overlay = overlay as? MKPolygon {
            let r = MKPolygonRenderer(polygon:overlay)
            r.fillColor = UIColor.red.withAlphaComponent(0.1)
            r.strokeColor = UIColor.red.withAlphaComponent(0.8)
            r.lineWidth = 2
            return r
        }
        return MKOverlayRenderer()
}

Custom Overlay Class

The triangle in Figure 20-4 is rather crude; I could draw a better arrow shape using a CGPath (Chapter 2). The built-in MKOverlayRenderer subclass that lets me do that is MKOverlayPathRenderer. To structure things similarly to the preceding example, I’d like to supply the CGPath when I add the overlay instance to the map view. No built-in class lets me do that, so I’ll use a custom class, MyOverlay, that adopts the MKOverlay protocol.

A minimal overlay class looks like this:

class MyOverlay : NSObject, MKOverlay {
    var coordinate : CLLocationCoordinate2D {
        get {
            let pt = MKMapPointMake(
                MKMapRectGetMidX(self.boundingMapRect),
                MKMapRectGetMidY(self.boundingMapRect))
            return MKCoordinateForMapPoint(pt)
        }
    }
    var boundingMapRect : MKMapRect
    init(rect:MKMapRect) {
        self.boundingMapRect = rect
        super.init()
    }
}

Our actual MyOverlay class will also have a path property; this will be a UIBezierPath that holds our CGPath and supplies it to the MKOverlayPathRenderer.

Just as the coordinate property of an annotation tells the map view where on earth the annotation is to be drawn, the boundingMapRect property of an overlay tells the map view where on earth the overlay is to be drawn. Whenever any part of the boundingMapRect is displayed within the map view’s bounds, the map view will have to concern itself with drawing the overlay. With MKPolygon, we supplied the points of the polygon in earth coordinates and the boundingMapRect was calculated for us. With our custom overlay class, we must supply or calculate it ourselves.

At first it may appear that there is a typological impedance mismatch: the boundingMapRect is an MKMapRect, whereas a CGPath is defined by CGPoints. However, it turns out that these units are interchangeable: the CGPoints of our CGPath will be translated for us directly into MKMapPoints on the same scale — that is, the distance between any two CGPoints will be the distance between the two corresponding MKMapPoints. However, the origins are different: the CGPath must be described relative to the top-left corner of the boundingMapRect. To put it another way, the boundingMapRect is described in earth coordinates, but the top-left corner of the boundingMapRect is .zero as far as the CGPath is concerned. (You might think of this difference as analogous to the difference between a UIView’s frame and its bounds.)

To make life simple, I’ll think in meters; actually, I’ll think in chunks of 75 meters, because this turns out to be a good unit for positioning and laying out this particular arrow. Thus, a line one unit long would in fact be 75 meters long if I were to arrive at this actual spot on the earth and discover the overlay literally drawn on the ground. Having derived this chunk (unit), I use it to lay out the boundingMapRect, four units on a side and positioned slightly east and north of the annotation point (because that’s where the road is). Then I simply construct the arrow shape within the 4×4-unit square, rotating it so that it points in roughly the same direction as the road:

// start with our position and derive a nice unit for drawing
let lat = self.annloc.latitude
let metersPerPoint = MKMetersPerMapPointAtLatitude(lat)
let c = MKMapPointForCoordinate(self.annloc)
let unit = CGFloat(75.0/metersPerPoint)
// size and position the overlay bounds on the earth
let sz = CGSize(4*unit, 4*unit)
let mr = MKMapRectMake(
    c.x + 2*Double(unit), c.y - 4.5*Double(unit),
    Double(sz.width), Double(sz.height))
// describe the arrow as a CGPath
let p = CGMutablePath()
let start = CGPoint(0, unit*1.5)
let p1 = CGPoint(start.x+2*unit, start.y)
let p2 = CGPoint(p1.x, p1.y-unit)
let p3 = CGPoint(p2.x+unit*2, p2.y+unit*1.5)
let p4 = CGPoint(p2.x, p2.y+unit*3)
let p5 = CGPoint(p4.x, p4.y-unit)
let p6 = CGPoint(p5.x-2*unit, p5.y)
let points = [
    start, p1, p2, p3, p4, p5, p6
]
// rotate the arrow around its center
let t1 = CGAffineTransform(translationX: unit*2, y: unit*2)
let t2 = t1.rotated(by:-.pi/3.5)
let t3 = t2.translatedBy(x: -unit*2, y: -unit*2)
p.addLines(between: points, transform: t3)
p.closeSubpath()
// create the overlay and give it the path
let over = MyOverlay(rect:mr)
over.path = UIBezierPath(cgPath:p)
// add the overlay to the map
self.map.add(over)

The delegate method, where we provide the MKOverlayPathRenderer, is simple. We pull the CGPath out of the MyOverlay instance and hand it to the MKOverlayPathRenderer, also telling the MKOverlayPathRenderer how to stroke and fill that path:

func mapView(_ mapView: MKMapView,
    rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
        if let overlay = overlay as? MyOverlay {
            let r = MKOverlayPathRenderer(overlay:overlay)
            r.path = overlay.path.cgPath
            r.fillColor = UIColor.red.withAlphaComponent(0.2)
            r.strokeColor = .black
            r.lineWidth = 2
            return r
        }
        return MKOverlayRenderer()
}

The result is a much nicer arrow (Figure 20-5), and of course this technique can be generalized to draw an overlay from any CGPath we like.

pios 3405
Figure 20-5. A nicer overlay

Custom Overlay Renderer

For full generality, you could define your own MKOverlayRenderer subclass; your subclass must override and implement draw(_:zoomScale:in:). The first parameter is an MKMapRect describing a tile of the visible map (not the size and position of the overlay); the third parameter is the CGContext into which you are to draw. Your implementation may be called several times simultaneously on different background threads, one for each tile, so be sure to draw in a thread-safe way. The overlay itself is available through the inherited overlay property, and MKOverlayRenderer instance methods such as rect(for:) are provided for converting between the map’s MKMapRect coordinates and the overlay renderer’s graphics context coordinates. The graphics context arrives already configured such that our drawing will be clipped to the current tile. (All this should remind you of CATiledLayer, Chapter 7.)

In our example, we can move the entire functionality for drawing the arrow into an MKOverlayRenderer subclass, which I’ll call MyOverlayRenderer. Its initializer takes an angle: parameter, with which I’ll set its angle property; now our arrow can point in any direction. Another nice benefit of this architectural change is that we can use the zoomScale: parameter to determine the stroke width. For simplicity, my implementation of draw(_:zoomScale:in:) ignores the incoming MKMapRect value and just draws the entire arrow every time it is called:

var angle : CGFloat = 0
init(overlay:MKOverlay, angle:CGFloat) {
    self.angle = angle
    super.init(overlay:overlay)
}
override func draw(_ mapRect: MKMapRect,
    zoomScale: MKZoomScale, in con: CGContext) {
        con.setStrokeColor(UIColor.black.cgColor)
        con.setFillColor(UIColor.red.withAlphaComponent(0.2).cgColor)
        con.setLineWidth(1.2/zoomScale)
        let unit =
            CGFloat(MKMapRectGetWidth(self.overlay.boundingMapRect)/4.0)
        let p = CGMutablePath()
        let start = CGPoint(0, unit*1.5)
        let p1 = CGPoint(start.x+2*unit, start.y)
        let p2 = CGPoint(p1.x, p1.y-unit)
        let p3 = CGPoint(p2.x+unit*2, p2.y+unit*1.5)
        let p4 = CGPoint(p2.x, p2.y+unit*3)
        let p5 = CGPoint(p4.x, p4.y-unit)
        let p6 = CGPoint(p5.x-2*unit, p5.y)
        let points = [
            start, p1, p2, p3, p4, p5, p6
        ]
        let t1 = CGAffineTransform(translationX: unit*2, y: unit*2)
        let t2 = t1.rotated(by:self.angle)
        let t3 = t2.translatedBy(x: -unit*2, y: -unit*2)
        p.addLines(between: points, transform: t3)
        p.closeSubpath()
        con.addPath(p)
        con.drawPath(using: .fillStroke)
}

To add the overlay to our map, we still must determine its MKMapRect:

let lat = self.annloc.latitude
let metersPerPoint = MKMetersPerMapPointAtLatitude(lat)
let c = MKMapPointForCoordinate(self.annloc)
let unit = 75.0/metersPerPoint
// size and position the overlay bounds on the earth
let sz = CGSize(4*CGFloat(unit), 4*CGFloat(unit))
let mr = MKMapRectMake(
    c.x + 2*unit, c.y - 4.5*unit,
    Double(sz.width), Double(sz.height))
let over = MyOverlay(rect:mr)
self.map.add(over, level:.aboveRoads)

The delegate, providing the overlay renderer, now has very little work to do; in our implementation, it merely supplies an angle for the arrow:

func mapView(_ mapView: MKMapView,
    rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
        if overlay is MyOverlay {
            let r = MyOverlayRenderer(overlay:overlay, angle: -.pi/3.5)
            return r
        }
        return MKOverlayRenderer()
}

Other Overlay Features

Our MyOverlay class, adopting the MKOverlay protocol, also implements the coordinate property by means of a getter method to return the center of the boundingMapRect. This is crude, but it’s a good minimal implementation. The purpose of this property is to specify the position where you would add an annotation describing the overlay. For example:

// ... create overlay and assign it a path as before ...
self.map.add(over, level:.aboveRoads)
let annot = MKPointAnnotation()
annot.coordinate = over.coordinate
annot.title = "This way!"
self.map.addAnnotation(annot)

The MKOverlay protocol also lets you provide an implementation of intersects(_:) to refine your overlay’s definition of what constitutes an intersection with itself; the default is to use the boundingMapRect, but if your overlay is drawn in some nonrectangular shape, you might want to use its actual shape as the basis for determining intersection.

Overlays are maintained by the map view as an array and are drawn from back to front starting at the beginning of the array. MKMapView has extensive support for adding and removing overlays, and for managing their layering order. When you add the overlay to the map, you can say where you want it drawn among the map view’s sublayers. This is also why methods for adding and inserting overlays have a level: parameter. The levels are (MKOverlayLevel):

  • .aboveRoads (and below labels)

  • .aboveLabels

The MKTileOverlay class, adopting the MKOverlay protocol, lets you superimpose, or even substitute (canReplaceMapContent), a map view’s drawing of the map itself. You provide a set of tiles at multiple sizes to match multiple zoom levels, and the map view fetches and draws the tiles needed for the current region and degree of zoom. In this way, for example, you could integrate your own topo map into an MKMapView’s display. It takes a lot of tiles to draw an area of any size, so MKTileOverlay is initialized with a URL, which can be a remote URL for tiles to be fetched across the Internet.

Map Kit and Current Location

A device may have sensors that can report its current location. Map Kit provides simple integration with these facilities. Keep in mind that the user can turn off these sensors or can refuse your app access to them (in the Settings app, under Privacy → Location Services), so trying to use these features may fail. Also, determining the device’s location can take time.

The real work here is being done by a CLLocationManager instance, which needs to be created and retained; the usual thing is to initialize a view controller instance property by assigning a new CLLocationManager instance to it:

let locman = CLLocationManager()

Moreover, you must obtain user authorization, and your Info.plist must state the reason why (as I’ll explain in more detail in Chapter 21):

self.locman.requestWhenInUseAuthorization()

You can then ask an MKMapView in your app to display the device’s location just by setting its showsUserLocation property to true; the map will automatically put an annotation at that location.

The userLocation property of the map view is an MKUserLocation, adopting the MKAnnotation protocol. It has a location property, a CLLocation, whose coordinate is a CLLocationCoordinate2D; if the map view’s showsUserLocation is true and the map view has actually worked out the user’s location, the coordinate describes that location. It also has title and subtitle properties, plus you can check whether it currently isUpdating. The default annotation appearance comes from the map view’s tintColor. You are free to supply your own annotation view to be displayed for this annotation, just as for any annotation. MKMapViewDelegate methods keep you informed of the map’s attempts to locate the user:

  • mapViewWillStartLocatingUser(_:)

  • mapViewDidStopLocatingUser(_:)

  • mapView(_:didUpdate:) (provides the new MKUserLocation)

  • mapView(_:didFailToLocateUserWithError:)

Displaying the appropriate region of the map — that is, actually showing the part of the world where the user is located — is a separate task. The simplest way is to take advantage of the MKMapView’s userTrackingMode property, which determines how the user’s real-world location should be tracked automatically by the map display; your options are (MKUserTrackingMode):

.none

If showsUserLocation is true, the map gets an annotation at the user’s location, but that’s all; the map’s region is unchanged. You could set it manually in mapView(_:didUpdate:).

.follow

Setting this mode sets showsUserLocation to true. The map automatically centers the user’s location and scales appropriately. When the map is in this mode, you should not set the map’s region manually, as you’ll be struggling against the tracking mode’s attempts to do the same thing.

.followWithHeading

Like .follow, but the map is also rotated so that the direction the user is facing is up. In this case, the userLocation annotation also has a heading property, a CLHeading; I’ll talk more about headings in Chapter 21.

Thus, this code is sufficient to start displaying the user’s location:

self.map.userTrackingMode = .follow

When the userTrackingMode is one of the .follow modes, if the user is left free to zoom and scroll the map, the userTrackingMode may be automatically changed back to .none (and the user location annotation may be removed). You’ll probably want to provide a way to let the user turn tracking back on again, or to toggle among the three tracking modes.

One way to do that is with an MKUserTrackingBarButtonItem, a UIBarButtonItem subclass. You initialize MKUserTrackingBarButtonItem with a map view, and its behavior is automatic from then on: when the user taps it, it switches the map view to the next tracking mode, and its icon reflects the current tracking mode. A delegate method tells you when the MKUserTrackingMode changes:

  • mapView(_:didChange:animated:)

You can also ask the map view whether the user’s location, if known, is in the visible region of the map (isUserLocationVisible).

Communicating with the Maps App

Your app can communicate with the Maps app. For example, instead of displaying a point of interest in a map view in our own app, we can ask the Maps app to display it. The user could then bookmark or share the location. The channel of communication between your app and the Maps app is the MKMapItem class.

Here, I’ll ask the Maps app to display the same point marked by the annotation in our earlier examples, on a standard map portraying the same region of the earth that our map view is currently displaying (Figure 20-6):

pios 3406
Figure 20-6. The Maps app displays our point of interest
let p = MKPlacemark(coordinate:self.annloc, addressDictionary:nil)
let mi = MKMapItem(placemark: p)
mi.name = "A Great Place to Dirt Bike" // label to appear in Maps app
mi.openInMaps(launchOptions:[
    MKLaunchOptionsMapTypeKey: MKMapType.standard.rawValue,
    MKLaunchOptionsMapCenterKey: self.map.region.center,
    MKLaunchOptionsMapSpanKey: self.map.region.span
])

If you start with an MKMapItem returned by the class method mapItemForCurrentLocation, you’re asking the Maps app to display the device’s current location. This call doesn’t attempt to determine the device’s location, nor does it contain any location information; it merely generates an MKMapItem which, when sent to the Maps app, will cause it to attempt to determine (and display) the device’s location:

let mi = MKMapItem.forCurrentLocation()
mi.openInMaps(launchOptions:[
    MKLaunchOptionsMapTypeKey: MKMapType.standard.rawValue
])

Geocoding, Searching, and Directions

Map Kit provides your app with three services that involve performing queries over the network. These services take time and might not succeed at all, as they depend upon network and server availability; moreover, results may be more or less uncertain. Therefore, they involve a completion function that is called back asynchronously on the main thread. The three services are:

Geocoding

Translation of a street address to a coordinate and vice versa. For example, what address am I at right now? Or conversely, what are the coordinates of my home address?

Searching

Lookup of possible matches for a natural language search. For example, what are some Thai restaurants near me?

Directions

Lookup of turn-by-turn instructions and route mapping from a source location to a destination location.

The completion function is called, in every case, with a single response object plus an Error. If the response object is nil, the Error tells you what the problem was.

Geocoding

Geocoding functionality is encapsulated in the CLGeocoder class. The response, if things went well, is an array of CLPlacemark objects, a series of guesses from best to worst; if things went really well, the array will contain exactly one CLPlacemark.

A CLPlacemark can be used to initialize an MKPlacemark, a CLPlacemark subclass that adopts the MKAnnotation protocol, and is therefore suitable to be handed directly over to an MKMapView for display.

Here is an (unbelievably simpleminded) example that allows the user to enter an address in a UISearchBar (Chapter 12) to be displayed in an MKMapView:

guard let s = searchBar.text else { return }
let geo = CLGeocoder()
geo.geocodeAddressString(s) { placemarks, error in
    guard let placemarks = placemarks else { return }
    let p = placemarks[0]
    let mp = MKPlacemark(placemark:p)
    self.map.addAnnotation(mp)
    self.map.setRegion(
        MKCoordinateRegionMakeWithDistance(mp.coordinate, 1000, 1000),
        animated: true)
}

By default, the resulting annotation’s callout title contains a nicely formatted string describing the address.

The converse operation is reverse geocoding: you start with a coordinate — actually a CLLocation, which you’ll obtain from elsewhere, or construct from a coordinate using init(latitude:longitude:) — and then, in order to obtain the corresponding address, you call reverseGeocodeLocation(_:completionHandler:).

The address is expressed through the CLPlacemark addressDictionary property, whose "FormattedAddressLines" key yields an array of strings, one per line of the printed address. Alternatively, you can consult directly such CLPlacemark properties as subthoroughfare (a house number), thoroughfare (a street name), locality (a town), and administrativeArea (a state).

In this example of reverse geocoding, we have an MKMapView that is already tracking the user, and so we have the user’s location as the map’s userLocation; we ask for the corresponding address:

guard let loc = self.map.userLocation.location else { return }
let geo = CLGeocoder()
geo.reverseGeocodeLocation(loc) { placemarks, error in
    guard let ps = placemarks, ps.count > 0 else {return}
    let p = ps[0]
    if let d = p.addressDictionary {
        if let add = d["FormattedAddressLines"] as? [String] {
            for line in add {
                print(line)
            }
        }
    }
}

Searching

The MKLocalSearch class, along with MKLocalSearchRequest and MKLocalSearchResponse, lets you ask the server to perform a natural language search for you. This is less formal than forward geocoding, described in the previous section; instead of searching for an address, you can search for a point of interest by name or description. It can be useful, for some types of search, to constrain the area of interest by setting the MKLocalSearchRequest’s region. In this example, I’ll do a natural language search for a Thai restaurant near the user location currently displayed in the map, and I’ll display it with an annotation in our map view:

guard let loc = self.map.userLocation.location else { return }
let req = MKLocalSearchRequest()
req.naturalLanguageQuery = "Thai restaurant"
req.region = MKCoordinateRegionMake(loc.coordinate, MKCoordinateSpanMake(1,1))
let search = MKLocalSearch(request:req)
search.start { response, error in
    guard let response = response else { print(error); return }
    self.map.showsUserLocation = false
    let mi = response.mapItems[0] // I'm feeling lucky
    let place = mi.placemark
    let loc = place.location!.coordinate
    let reg = MKCoordinateRegionMakeWithDistance(loc, 1200, 1200)
    self.map.setRegion(reg, animated:true)
    let ann = MKPointAnnotation()
    ann.title = mi.name
    ann.subtitle = mi.phoneNumber
    ann.coordinate = loc
    self.map.addAnnotation(ann)
}
Tip

Introduced in iOS 9.3, MKLocalSearchCompleter lets you use the MKLocalSearch remote database to suggest completions as the user types a search query.

Directions

The MKDirections class, along with MKDirectionsRequest and MKDirectionsResponse, looks up walking or driving directions between two locations expressed as MKMapItem objects. The resulting MKDirectionsResponse includes an array of MKRoute objects; each MKRoute includes an MKPolyline suitable for display as an overlay in your map, as well as an array of MKRouteStep objects, each of which provides its own MKPolyline plus instructions and distances. The MKDirectionsResponse also has its own source and destination MKMapItems, which may be different from what we started with.

To illustrate, I’ll continue from the Thai food example in the previous section, starting at the point where we obtained the Thai restaurant’s MKMapItem:

// ... same as before up to this point ...
let mi = response.mapItems[0] // I'm still feeling lucky
let req = MKDirectionsRequest()
req.source = MKMapItem.forCurrentLocation()
req.destination = mi
let dir = MKDirections(request:req)
dir.calculate { response, error in
    guard let response = response else { print(error); return }
    let route = response.routes[0] // I'm feeling insanely lucky
    let poly = route.polyline
    self.map.add(poly)
    for step in route.steps {
        print("After (step.distance) meters: (step.instructions)")
    }
}

The step-by-step instructions appear in the console; in real life, of course, we would presumably display these in our app’s interface. The route is drawn in our map view, provided we have an appropriate implementation of mapView(_:rendererFor:), such as this:

func mapView(_ mapView: MKMapView,
    rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
        if let overlay = overlay as? MKPolyline {
            let r = MKPolylineRenderer(polyline:overlay)
            r.strokeColor = UIColor.blue.withAlphaComponent(0.8)
            r.lineWidth = 2
            return r
        }
        return MKOverlayRenderer()
}

You can also ask MKDirections to estimate the time of arrival, by calling calculateETA(completionHandler:), and iOS 9 introduced arrival time estimation for some public transit systems (and you can tell the Maps app to display a transit directions map).

Tip

Instead of your app providing geocoding, searching, or directions, you can ask the Maps app to provide them: form a URL and call UIApplication’s open(_:options:completionHandler:). For the structure of this URL, see the “Map Links” chapter of Apple’s Apple URL Scheme Reference. You can also use this technique to ask the Maps app to display a point of interest, as discussed in the previous section.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.239.178