Chapter 8. Maps and Location

In this chapter, we will have a look at some awesome updates to the MapKit and CoreLocation frameworks.

8.1 Requesting the User’s Location a Single Time

Problem

You want an optimized and energy-efficient way of requesting the current location of the user only once.

Solution

Use the requestLocation() method of the CLLocationManager class. The new location will be sent to your location manager’s locationManager(_:didUpdateLocations:) delegate method. Errors will be reported on locationManager(_:didFailWithError:). You can make only one request to this method at any given time. A new request will cancel the previous one.

Discussion

Place a button on your interface inside IB and then hook it up to a method in your code called requestLocation(). Then go into your Info.plist file and set the value of the NSLocationWhenInUseUsageDescription key to a valid string that explains to the user why you want to get her location. You will also have to import the CoreLocation framework and make your view controller conform to CLLocationManagerDelegate.

Implement a variable in your view controller to represent the location manager:

  lazy var locationManager: CLLocationManager = {
    let m = CLLocationManager()
    m.delegate = self
    m.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
    return m
  }()

When your button is pressed, request access to the user’s location. This requests users location to be delivered to your app only when it is the foreground app. As soon as your app is sent to the background, iOS stops delivering location updates to you:

  @IBAction func requestLocation() {

    locationManager.requestWhenInUseAuthorization()

  }

Then wait for the user to accept or reject the request. If everything is going smoothly, request the user’s location:

  func locationManager(manager: CLLocationManager,
    didChangeAuthorizationStatus status: CLAuthorizationStatus) {

      if case .AuthorizedWhenInUse = status{
        manager.requestLocation()
      } else {
        //TODO: we didn't get access, handle this
      }

  }

Last but not least, wait for the location gathering mechanism to fail or succeed:

  func locationManager(manager: CLLocationManager,
    didUpdateLocations locations: [CLLocation]) {
      //TODO: now you have access to the location. do your work
  }

  func locationManager(manager: CLLocationManager,
    didFailWithError error: NSError) {
    //TODO: handle the error
  }

See Also

Recipe 8.2

8.2 Requesting the User’s Location in Background

Problem

You want to receive updates on the user’s location while in the background. Being a good iOS citizen, you won’t ask for this unless you really need it for the main functionality of your app.

Solution

Set the allowsBackgroundLocationUpdates property of your location manager to true and ask for location updates using the requestAlwaysAuthorization() function.

Discussion

When linked against iOS 9, apps that want to ask for a user’s location when the app is in the background have to set the allowsBackgroundLocationUpdates property of their location manager to true. We are going to have to have a look at an example. Start a single view controller app, place a button on your UI with IB, and give it a title similar to “Request background location updates”. Then hook it to a method in your view controller and name the method requestBackgroundLocationUpdates(). In your Info.plist file, set the string value of the NSLocationAlwaysUsageDescription key and make sure that it explains exactly why you want to access the user’s location even in the background. Then go into the Capabilities section of your target, and under Background Modes, enable “Location updates” (see Figure 8-1).

Figure 8-1. Enabling location updates in Background Modes in your project

Now import CoreLocation in your code and make your view controller conformant to CLLocationManagerDelegate. Create your location manager and make sure that the allowsBackgroundLocationUpdates property is set to true.

  lazy var locationManager: CLLocationManager = {
    let m = CLLocationManager()
    m.delegate = self
    m.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
    m.allowsBackgroundLocationUpdates = true
    return m
    }()

When the user presses the button, ask for location updates:

  @IBAction func requestBackgroundLocationUpdates() {
    locationManager.requestAlwaysAuthorization()
  }

Wait until the user accepts the request and then start looking for location updates:

  func locationManager(manager: CLLocationManager,
    didChangeAuthorizationStatus status: CLAuthorizationStatus) {
      if case CLAuthorizationStatus.AuthorizedAlways = status{
        manager.startUpdatingLocation()
      }
  }

Last but not least, implement the usual location manager methods to get to know when the user’s location has changed:

  func locationManager(manager: CLLocationManager,
    didUpdateLocations locations: [CLLocation]) {
      //TODO: now you have access to the location. do your work

  }

  func locationManager(manager: CLLocationManager,
    didFailWithError error: NSError) {
      //TODO: handle the error
  }

See Also

Recipe 8.1 

8.3 Customizing the Tint Color of Pins on the Map

Problem

You want to set the tint color of pin annotations on your map manually.

Solution

Use the pinTintColor property of the MKPinAnnotationView class like so:

      let view = MKPinAnnotationView(annotation: annotation,
        reuseIdentifier: color.toString())

      view.pinTintColor = color

Discussion

Let’s check out an example. Create a single view controller project and dump a map view on top of your view. Make sure that you set the delegate of this map view to your view controller. Also link it to a variable named map in your view controller.

In the view controller, we are going to create annotations with reusable identifiers, so let’s use the color as the ID:

import MapKit

extension UIColor{
  final func toString() -> String{

    var red = 0.0 as CGFloat
    var green = 0.0 as CGFloat
    var blue = 0.0 as CGFloat
    var alpha = 0.0 as CGFloat
    getRed(&red, green: &green, blue: &blue, alpha: &alpha)

    return "(Int(red))(Int(green))(Int(blue))(Int(alpha))"
  }
}

Now we create our annotation:

class Annotation : NSObject, MKAnnotation{
  var coordinate: CLLocationCoordinate2D
  var title: String?
  var subtitle: String?

  init(coordinate: CLLocationCoordinate2D, title: String, subtitle: String){
    self.coordinate = coordinate
    self.title = title
    self.subtitle = subtitle
  }

}

Now ensure that your view controller conforms to the MKMapViewDelegate protocol, define the location that you want to display on the map, and create an annotation for it:

  let color = UIColor(red: 0.4, green: 0.8, blue: 0.6, alpha: 1.0)
  let location = CLLocationCoordinate2D(latitude: 59.33, longitude: 18.056)

  lazy var annotations: [MKAnnotation] = {
    return [Annotation(coordinate: self.location,
      title: "Stockholm Central Station",
      subtitle: "Stockholm, Sweden")]
    }()

When your view appears on the screen, add the annotation to the map:

  override func viewDidAppear(animated: Bool) {
    super.viewDidAppear(animated)

    map.removeAnnotations(annotations)
    map.addAnnotations(annotations)

  }

And when the map view asks for an annotation view for your annotation, return an annotation view with the custom color (see Figure 8-2):

  func mapView(mapView: MKMapView,
    viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {

      let view: MKPinAnnotationView
      if let v = mapView.dequeueReusableAnnotationViewWithIdentifier(
        color.toString()) where v is MKPinAnnotationView{
          view = v as! MKPinAnnotationView
      } else {
        view = MKPinAnnotationView(annotation: annotation,
          reuseIdentifier: color.toString())
      }

      view.pinTintColor = color

      return view

  }
Figure 8-2. Our custom color pin is displayed on the map

See Also

Recipe 8.4 and Recipe 8.5

8.4 Providing Detailed Pin Information with Custom Views

Problem

When the user taps on an annotation in a map, you want to display details for that annotation in a view.

Solution

Set the detailCalloutAccessoryView property of your MKAnnotationView instances to a valid UIView instance.

Discussion

Create your project just as you did in Recipe 8.3. In this recipe, I am going to reuse a lot of code from the aforementioned recipe, except for the implementation of the mapView(_:viewForAnnotation:) delegate method of our view controller. Instead, we are going to construct instances here of MKAnnotationView and then set the detail callout accessory view:

  func mapView(mapView: MKMapView,
    viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {

      let view: MKAnnotationView
      if let v = mapView
        .dequeueReusableAnnotationViewWithIdentifier(identifier){
          //reuse
          view = v
      } else {
        //create a new one
        view = MKAnnotationView(annotation: annotation,
          reuseIdentifier: identifier)

        view.canShowCallout = true

        if let img = UIImage(named: "Icon"){
          view.detailCalloutAccessoryView = UIImageView(image: img)
        }

        if let extIcon = UIImage(named: "ExtIcon"){
          view.image = extIcon
        }
      }

      return view

  }

Figure 8-3 shows the image of an annotation on a map. The image inside the callout is the detail callout accessory view.

Figure 8-3. Annotation with detail callout accessory
Note

I am using two public domain images in this recipe. You also can find public domain images on Google.

See Also

Recipe 8.3 and Recipe 8.5

8.5 Displaying Traffic, Scale, and Compass Indicators on the Map

Problem

You want to display traffic as well as the little compass and scale indicators on the map view.

Solution

Set the following properties of your map view to true:

  • showsCompass
  • showsTraffic
  • showsScale

Discussion

Place a map view on your view and set the appropriate constraints on it so that it stretches across the width and height of your view controller’s view. This is really optional, but useful so the user can see the map view properly on all devices. Then follow what I talked about in Recipe 8.3 to place an annotation on the map. Write a code similar to the following in a method such as viewDidLoad:

    map.showsCompass = true
    map.showsTraffic = true
    map.showsScale = true

The results will be similar to those shown in Figure 8-4. The scale is shown on top left and the compass on the top right. You have to rotate the map for the compass to appear.

Figure 8-4. Map with scale, compass, and traffic

See Also

Recipe 8.3 and Recipe 8.4

8.6 Providing an ETA for Transit Transport Type

Problem

You want your app to provide routing options to the user when she is in the iOS maps app.

Solution

Mark your app as a routing app and construct an instance of the MKDirectionsRequest class. Set the transportType property of that request to Transit and send your request to Apple to calculate an estimated time of arrival (ETA), using the calculateETAWithCompletionHandler(_:) method of the MKDirections class.

Note

We use Geo JSON files in this recipe, so read the spec for that format first, please.

Discussion

Create a single-view application. Then head to the Capabilities tab in Xcode, enable the Maps section, and mark the routing options that you believe your app will be able to provide (see Figure 8-5). I’ve enabled all these items for demonstration purposes. You probably wouldn’t want to enable all of these in your app.

Figure 8-5. Transportation routing options

Create a new Directions.geoJson file in your app and then head over to GeoJson.io to create the polygon that defines your routing coverage area. Then copy and paste the generated content and place it in the aforementioned file in your project. Now go and edit your target’s scheme. Under Run and then Options, find the Routing App Coverage file section and select your file (see Figure 8-6).

Figure 8-6. Here I am selecting the routing coverage file for my project
Note

You can always go to GeoJsonLint to validate your Geo JSON files.

This will allow the maps app to open my app whenever the user asks for transit information on the iOS maps app. Now code the application(_:openURL:options:) method of your app delegate and handle the routing request there:

  func application(app: UIApplication, openURL url: NSURL,
    options: [String : AnyObject]) -> Bool {

      guard MKDirectionsRequest.isDirectionsRequestURL(url) else{
        return false
      }

      //now we have the url
      let req = MKDirectionsRequest(contentsOfURL: url)

      guard req.source != nil && req.destination != nil else{
        return false
      }

      req.transportType = .Transit
      req.requestsAlternateRoutes = true

      let dir = MKDirections(request: req)

      dir.calculateETAWithCompletionHandler {response, error in
        guard let resp = response where error == nil else{
          //handle the error
          print(error!)
          return
        }

        print("ETA response = (resp)")

      }

      return true
  }

Now open the maps app and ask for directions from one location to another. If the maps app couldn’t handle the request, it will show a little “View Routing Apps” button. Regardless of whether the maps app could or couldn’t show the routing options, the user can always press the little navigation button to open alternative routing apps (see Figure 8-7). Your app will be displayed in the list of routing apps if the user asks for a routing option you support, and if the starting and stopping points are within the shape you defined in your Geo JSON file. When the user opens your app, your app delegate will be informed and will calculate an ETA.

Figure 8-7. Our app, displayed in the list of routing apps

See Also

Recipe 8.5

8.7 Launching the iOS Maps App in Transit Mode

Problem

You want to launch iOS’s maps app in transit mode.

Solution

When calling the openMapsWithItems(_:launchOptions:) class method of MKMapItem, in the options collection, set the value of the MKLaunchOptionsDirectionsModeKey key to MKLaunchOptionsDirectionsModeTransit.

Discussion

Let’s create a single-view controller app and place a button on the view controller to open a map. Set the title of this button to something like “Open maps app in transit mode.” Then hook it up to your view controller. For every coordinate of type CLLocationCoordinate2D, you have to create an instance of MKPlacemark and then from the placemark, create an instance of MKMapItem.

Here is the source map item:

    let srcLoc = CLLocationCoordinate2D(latitude: 59.328564,
      longitude: 18.061448)
    let srcPlc = MKPlacemark(coordinate: srcLoc, addressDictionary: nil)
    let src = MKMapItem(placemark: srcPlc)

Followed by the destination map item:

    let desLoc = CLLocationCoordinate2D(latitude: 59.746148,
      longitude: 18.683281)
    let desPlc = MKPlacemark(coordinate: desLoc, addressDictionary: nil)
    let des = MKMapItem(placemark: desPlc)
Note

You can use the Get Latitude Longitude website to find the latitude and longitude of any point on the map.

Now we can launch the app, under transit mode, with the source and the destination points:

    let options = [
      MKLaunchOptionsDirectionsModeKey : MKLaunchOptionsDirectionsModeTransit
    ]

    MKMapItem.openMapsWithItems([src, des], launchOptions: options)

See Also

Recipe 8.5 and Recipe 8.6

8.8 Showing Maps in Flyover Mode

Problem

You want to display your maps in a flyover state, where the regions on the map are translated onto a 3D globe, rather than a 2D flattened map.

Solution

Set the mapType property of your MKMapView to either HybridFlyover or SatelliteFlyover.

Discussion

The flyover mode of a map view represents the map as if it were on a globe, rather than flat. So keep that in mind when placing a camera on the map to show to the user.

Let’s start off with a single-view controller app. Place a map view on your view and hook it up to your code. I’ve named mine “map.” When your view gets loaded, make sure that your map type is one of the aforementioned flyover modes:

    map.mapType = .SatelliteFlyover
    map.showsBuildings = true

Then when your view appears on the screen, set the camera on your map:

    let loc = CLLocationCoordinate2D(latitude: 59.328564,
      longitude: 18.061448)

    let altitude: CLLocationDistance  = 500
    let pitch = CGFloat(45)
    let heading: CLLocationDirection = 90

    let c = MKMapCamera(lookingAtCenterCoordinate: loc,
      fromDistance: altitude, pitch: pitch, heading: heading)

    map.setCamera(c, animated: true)

Run this code on a real device (this doesn’t work very well on simulator) and you’ll get a display along the lines of Figure 8-8.

Figure 8-8. The Stockholm central station is shown here under satellite flyover mode
..................Content has been hidden....................

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