Playing with iOS MapKit Part 2: Reverse Geocoding and Custom Annotation Callouts

Playing With MapKit

Part 1 MapKit Tutorials 

Part 2 Reverse Geocoding and Custom Annotation Callouts

Part 3 Making it Pretty

Part 4 Race Conditions, Other Bugs, and Wrap-up

After following the tutorials in part 1, I wanted to keep exploring MapKit. As reverse geocoding was my next topic, I wanted to show the address of the current location in a callout (view that pops up above the pin).

To do this, I’ll need:

  1. A way to know when the current location was tapped
  2. A way to know the address of the current location
  3. A way to display the address in a callout

1. How do I know when the current location is tapped?

MKMapViewDelegate has a method called:

- (MKAnnotationView *) mapView:(MKMapView *)mapView 
viewForAnnotation:(id<MKAnnotation>)annotation

that is called when an annotation is tapped.

How do I tell that this annotation is my current location?

Notice that the annotation is any class that conforms to MKAnnotation.

Current location comes in a special class called MKUserLocation so you just need to test if the annotation is that class.

if ([annotation isKindOfClass:[MKUserLocation class]]
) {
...
}

Within this if statement, I need to turn the currentLocation into an address.

This MKUserLocation has a CLLocation property, a BOOL property for whether it’s updating, a CLHeading property, a title and a subtitle.

2. How do I find the address with coordinates? (Reverse Geocoding)

Apple provides a CLGeocoder class that has the method

- (void)reverseGeocodeLocation:(CLLocation *)location 
completionHandler:(CLGeocodeCompletionHandler)
completionHandler

All you need is the location which comes from MKUserLocation’s location property.

What does the completion handler return?

NSArray *placemarks, NSError *error

placemarks is an NSArray of CLPlacemarks. Each placemark has an addressDictionary. The key @”FormattedAddressLines” returns an array of formatted address lines.

Typically, there are three lines. One for address, another for city, state, and zip code, and one more for the country. In some situations, there are four lines where the first is the name of a venue.

3. How do I display the address?

One way is to set the callout for the annotation is to set the title of the annotation to the text you want in the callout. For example:

self.address = [NSString stringWithFormat:@" %@\n %@\n 
%@", street, cityState, country];
[(MKUserLocation *)annotation setTitle:self.address];

When I do this, the problem is that there is only one line and the rest of the address gets cut off.

Cut Off Callout

Adding line breaks to the title did not make it appear on additional lines, but only displayed the first line instead.

Callout title with line breaks

Messing with the left and right accessory buttons did not help either.

Callout with left and right accessory views colored red and blue
Callout with left and right accessory views colored red and blue

With no way to set more than two lines of text, I had to make a custom MKPinAnnotationView.

How do I make a custom annotation callout?

Since the callout is a property of the MKPinAnnotationView, I had to make a subclass of the MKPinAnnotationView and overwrite the setSelected: animated method.

- (void) setSelected:(BOOL)selected animated:(BOOL)animated
  1. Draw a label
  2. Set the text of the label with the annotation’s title
  3. Place a view as a subview of the MKPinAnnotationView subclass (which I called MultilineAnnotationView)
  4. Add the label as a subview of view.
setSelected
Why MKPinAnnotationView and not the default user location view?

The default user location view is a class that you cannot access or subclass.

Custom Callout View
The custom callout works! (but not perfectly)

Up next, making things look pretty.

Playing with iOS MapKit Part 1: MapKit Tutorials

Playing With MapKit

Part 1 MapKit Tutorials

Part 2 Reverse Geocoding and Custom Annotation Callouts

Part 3 Making it Pretty

Part 4 Race Conditions, Other Bugs, and Wrap-up

A main difference between a smartphone and any other phone is that smart phones can tell you where they are. What does location tell you? Location provides context about what you are doing, where you’re going, and by extension, who you are.

Apple makes it easy to show this information on a map with MapKit.

At the Flatiron School, I got a good foundation for learning frameworks, but hadn’t worked with MapKit yet. Since it felt core to many mobile apps, I decided to explore it.

Demo Video

Here is a demo of the finished app. All the code is on Github.

First Stop on the MapKit Train: Tutorials

Whenever I get into a new framework, I learn best by jumping right in and  doing it. Tutorials are a great way to get started. They help me focus by limiting scope and stay productive before my natural curiosity wants to go down some rabbit holes. I also don’t want to reinvent the wheel.

If I have seen further it is by standing on the shoulders of giants.

-Isaac Newton

Searching for iOS Mapkit tutorial, I find some by Ray Wenderlich, TechTopia, and AppCoda. All three have been really useful in the past. I went with the TechTopia one because it was written about iOS 7.

Tutorial 1: Make a MKMapView and Show the Current Location

Basically:

  • Add the MapKit framework
  • Make a MKMapView with Storyboards and import MKMapView
  • Assign the MKMapViewDelegate to the view controller
  • Set the MKMapView to show the current Location
  • *Not In Tutorial: Asking for Permission to Access User Location
  • *Set the location in Simulator
  • Zoom in on the MKMapView by changing the region
  • Change the MKMapView type
  • Update the MKMapView using the MKMapViewDelegate methods

*Most of this is already well explained in the tutorial. I want to point out two bumps on the road.

1. Simulator does not have a GPS, so you have to set the location by the menu bar: Debug -> Location -> Custom Location.

Google maps will give you latitude and longitude for any location if you click on the map. If it’s an icon, right click and click “What’s here?”.

Note: Sometimes the location doesn’t show up in the simulator the first time I run it. When I run it on another sized simulator, it works. Don’t know why this is.

2. More importantly, iOS 8 requires that you get permission to use the user location.

How to Get the Current Location Authorization Status

    CLAuthorizationStatus status = [CLLocationManager 
authorizationStatus];

CLAuthorizationStatus is an enum of

  • kCLAuthorizationStatusNotDetermined = 0
  • kCLAuthorizationStatusRestricted = 1
  • kCLAuthorizationStatusDenied = 2
  • kCLAuthorizationStatusAuthorized = 3 (Deprecated iOS 8)
  • kCLAuthorizationStatusAuthorizedAlways = kCLAuthorizationStatusAuthorized = 4
  • kCLAuthorizationStatusAuthorizedWhenInUse = 5

Statuses start out as kCLAuthorizationStatusNotDetermined

How to Ask the User for Location Authorization

If you want current location data only when the customer is using the app:

[self.locationManager requestWhenInUseAuthorization];

If you want current location data even when the customer is not using the app:

[self.locationManager requestAlwaysAuthorization];

Here I have my CLLocationManager as a property of the class. Change self.locationManager to the name of your locationManager.

How to Add Properties to your Info.plist

Even though you think you’ve requested authorization, you are likely still missing one more piece.

Properties List

In the Supporting Files Folder of your app directory, there is a Info.plist file.

It’s a properties list that your app uses through out the app.

You have to have a property in there for NSLocationWhenInUseUsageDescription (or NSLocationAlwaysUsageDescription depending on which permission your asking for), which tells the customer why you are asking for their location information.

Permission Popup

How to tell if you received location authorization

There is a delegate method for the location manager that tell you when the authorization status changes.

- (void)locationManager:(CLLocationManager *)manager 
didChangeAuthorizationStatus:(CLAuthorizationStatus)
status

The status here is the CLAuthorizationStatus.

Remember to set the locationManager’s delegate to the class that implements the delegate method. In this case, I’ve set the delegate to self inside my view controller.

self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;

Tutorial 2: Add Local Search with MKLocalSearchRequest and Display Results as MKPointAnnotation

Basically,

  • Add a textField so the user can input search terms
  • Add an IB action for the textFieldReturn to call the performSearch method
  • Add a performSearch method that sends a MKLocalSearchRequest and handles the MKLocalSearchResponse by parsing its array of MKMapItems into MKPointAnnotations

This was pretty straight forward. Apple makes the searches super easy by giving you completion handlers.

How to Perform a Local Search

You just need a MKLocalSearch object and a MKLocalSearchRequest object.

MKLocalSearch *search = [[MKLocalSearch alloc] 
initWithRequest:request];

and call

- (void)startWithCompletionHandler: 
(MKLocalSearchCompletionHandler)completionHandler

Tutorial 3: Find Directions with MKDirectionsRequest and Draw Them on the Map

Basically,

  • Add a ResultsTableViewController to show the names and phone numbers of the venues
  • Add a RouteViewController to show the route to the destination
  • Set up a MKDirectionsRequest with a source and destination in the RouteViewController
  • Make a MKDirections instance that calculates directions
  • Pass the MKDirectionsResponse to a showRoute method that adds an overlay of the polylines of the MKRoutes in the response to the map
  • Set up how the overlay will look

Notes

The tutorial sets up a custom UITableViewCell, but that’s not really necessary because you can set the title as the name and the subtitle to the phoneNumber.

Why am I in the middle of the ocean?

It’s important that you implement the MKMapView delegate method

- (void)mapView:(MKMapView *)mapView didUpdateUserLocation:
(MKUserLocation *)userLocation

Otherwise, you’ll find yourself in the ocean next to West Africa a lot (that’s what happens when your coordinates are (0,0)).

How to Get Directions

Make an instance of MKDirections and call:

- (void)calculateDirectionsWithCompletionHandler:
(MKDirectionsHandler)completionHandler

Up next: Going beyond the tutorials