Playing with iOS MapKit Part 3: Making it Pretty

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 getting down the basic functionality, I wanted to polish up the app to give it a quality feel and to dig deeper into the nitty-gritty details of MapKit.

So I made a list of things that could be improved and proceeded to knock each of them out one by one.

To do list

*I had the most fun with this one.

  1. Remove the jumpy effect on loading
  2. Move bar buttons from toolbar to buttons on the map
  3. Make the callout for the first search result come up
  4. Make the zip code and city on the same line
  5. Make the map center and zoom when the current location is tapped
  6. After clicking the textbox, resign the first responder when the map is clicked
  7. Add Directions
  8. *Zoom to include the destination in the Routeviewcontroller
  9. Intelligently choose walking vs driving directions
  10. Make the starting region of the route, the same as that of the previous view

1. Remove the jumpy effect on loading

When the app loaded, the default map showed the United States and when the user location was updated, it would move center on the current location, leading to a jumpy effect (I didn’t know about setCenterCoordinate: animated: at the time). The jump came about a couple seconds after opening the app depending on the internet connection and lead to a laggy feeling.

Design Choice

Initially, I chose to not center the map on the current location because it was a quick fix. I let the user tap the current location to zoom in. I chose this because it was the easiest fix.

After some iterations, I decided it was better just to zoom in automatically.

Back to list

2. Move bar buttons from toolbar to buttons on the map

Many of the map apps that I’ve seen have floating buttons on the map instead of a toolbar. I planned to do this, but it didn’t seem worth the effort.

Design Choice

After looking at the default maps app and seeing that it used a toolbar too, I decided to save time and stick with the toolbar. I just separated the buttons to make it more symmetrical.

Back to list

3. Make the callout for the first search result come up

Before, I had the callout for the last search result come up. This lead to the map whipping to the last location when it was off the visible region.

Design Choice

Make the first search result come up.

I used enumerateObjectsUsingBlock to save the first item. In doing this, I found out that I needed to set my firstAnnotation object to __block, so that the change would be visible outside of the block. (More on StackOverflow.)

How do I select a MKPointAnnotation?

[self.mapView selectAnnotation:firstAnnotation 
animated:YES];

Back to list

4. Make the zip code and city on the same line

Custom Callout View

The view for the address callout was too small to show the whole line for city, state, and zip code.

Design Choice

The easiest way to fix this was to make the view wider and shorten the zip to five numbers.

Afterwards, I ran into problems with special places that had an extra line for the placename, so I had to add extra code to parse the formatted address lines in cases where there were two or three lines.

In the final version, the callout view is now a bit too big, so future work may be nice to set the size dynamically and even animated.

Back to list

5. Make the map center and zoom when the current location is tapped

Easy, call the zoomIn method that the zoom in button calls.

Back to list

6. After clicking the textbox, resign the first responder when the map is clicked

The touchesBegan: withEvent: method detects when I touch outside the textfield onto the map, so this was easy to fix.

- (void) touchesBegan:(NSSet *)touches withEvent:
(UIEvent *)event
{
    [self.searchText resignFirstResponder];
}

Back to list

7. Add Directions

With the route shown, it was natural to want to show the directions and the distances involved.

Design Choice

The easiest thing was to show the directions in a tableViewController and segue to it from a UIBarButton on the navigation bar of the directions page. This is pretty standard, so I just passed the instructions to a table view controller and displayed it in the cells.

The distances are currently in meters. That’s not very intuitive so it would be nice to have this in blocks.

Back to list

*8. Zoom to include the destination in the RouteViewController

The idea is that when I selected a venue which was outside the visible range, I wanted the map to zoom to fit the destination and my current location. While tedious, executing this was not hard when I saw down to work backwards from

[self.routeMap setRegion:self.adjustedRegion 
animated:YES];

How do I zoom in to fit two locations on a map?

Basically,

  • Get the coordinates for your location and the destination location.
  • Find the difference in latitude and longitude and scale this up by a multiplier. A multiplier of 1 will have both locations on the edges of the screen.
  • Make sure to get the absolute difference. Negative numbers are no good.
  • Make a MKCoordinateSpan with the ABSOLUTE differences in latitude and longitude.
  • Find the average latitude and longitude to get the center for the region.
  • Make the MKCoordinateRegion with the centerCoordinate and the span.
  • Find the adjustedRegion by calling regionThatFits on the region in the last step.
  • Set the MKMapView to that adjustedRegion.
- (void)resizeRegionWithDestination:(MKMapItem *)destination 
userLocation:(MKUserLocation *)userLocation
{
    NSLog(@"need to resize");
    CLLocationCoordinate2D destinationCoordinate = 
        destination.placemark.location.coordinate;
    CLLocationCoordinate2D userCoordinate = 
        userLocation.location.coordinate;
 
    double scaleFactor = 2;
    CLLocationDegrees latitudeDelta = 
        (userCoordinate.latitude - 
        destinationCoordinate.latitude)*scaleFactor;
    CLLocationDegrees longitudeDelta = 
        (userCoordinate.longitude - 
        destinationCoordinate.longitude) *scaleFactor;
 
    if (latitudeDelta < 0) {
        latitudeDelta = latitudeDelta * -1;
    }
    if (longitudeDelta < 0) {
        longitudeDelta = longitudeDelta * -1;
    }
 
    MKCoordinateSpan span = 
    MKCoordinateSpanMake(latitudeDelta, longitudeDelta);
    CLLocationDegrees averageLatitude = 
        (userCoordinate.latitude + 
        destinationCoordinate.latitude)/2;
    CLLocationDegrees averageLongitude = 
        (userCoordinate.longitude + 
        destinationCoordinate.longitude)/2;
    CLLocationCoordinate2D centerCoordinate = 
    CLLocationCoordinate2DMake(averageLatitude, 
        averageLongitude);

    MKCoordinateRegion region = 
    MKCoordinateRegionMake(centerCoordinate, span);

    self.adjustedRegion = [self.routeMap regionThatFits:region];
    [self.routeMap setRegion:self.adjustedRegion animated:YES];
}

The cool part about this code is that it doesn’t care if it’s zooming in or out. It just zooms perfectly to the right region. I had to play with the scalingFactor a couple times to get it to look right. 2 seems to work pretty well.

Back to list

9. Intelligently choose walking vs driving directions

To do this, I needed to get the estimated time for walking directions.

A note on directions. If the MKDirectionRequest has the transportationType set to walking and the destination is not walkable, then the service will return no results. That’s why the transportation type is defaulted to any.

MKDirectionsRequest *request = [[MKDirectionsRequest alloc] init];
 
request.source = [MKMapItem mapItemForCurrentLocation];
request.destination = self.destination;
request.requestsAlternateRoutes = NO;
request.transportType = MKDirectionsTransportTypeWalking;
 
MKDirections *estimatedTimeOfArrival = [[MKDirections alloc] initWithRequest:request];
 
[estimatedTimeOfArrival calculateETAWithCompletionHandler:^(MKETAResponse *response, NSError *error) {
    //do something with the response
}];

After I knew that there was no error, I would check to see if the response.expectedTravelTime was greater than 15 minutes (how much I’d be willing to walk). If it was, I would change the MKDirectionsRequest’s transportType to include driving and find the directions with the modified request.

With the asynchronous call within the completion block, this might be a good candidate for wrapping the asynchronous calls using promises.

Back to list

10. Make the starting region of the route, the same as that of the previous view

Pretty simple. Just pass the region from the first MKMapView to the search results view controller to the RouteViewController through the prepare for segues.

Back to list

At the end of this process, I had an app that I could actually show people and it feels really good.

Up next squashing bugs.

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