Playing with iOS MapKit Part 4: Race Conditions, Other Bugs, Wrap-up

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

As the functionality and the look was coming into place, I took note of bugs that I saw.

The most insidious bugs are ones that don’t happen all the time or under conditions (ex. when internet is especially slow or the computer is especially fast). One example is the race condition.

What is a Race Condition?

A race condition is a situation when the sequence of asynchronous responses results in an undesired effect.

My Example

Very quickly after starting the app, I was tapping the current location dot and I was seeing “Current Location” in the bubble. It should have said the address that the address was closest to. If I waited a couple seconds to tap the current location, it would display the address as expected.

This led me to believe that I had a race condition.

Under normal conditions:

  1. The map updates the user location calling its delegate mapView:didUpdateUserLocation: which starts the reverseGeocoding call based on the user’s location.
  2. The reverseGeocoding response is received and the completion block sets the annotation’s title with the address of the user location.
  3. When the user location dot is tapped, it will display a custom bubble using the annotation’s title in the label.

Usually the internet is fast enough that the reverseGeocoding response returns before the user location dot is tapped.

The two things that are racing are

  1. the reverseGeocoding network call and
  2. the tap of the current location dot.

If 1 finished first, then the app would work as intended. If 2 finished first, the experience would be suboptimal.

Design Choice

One way to fix this is to update the label when the reverseGeocoding network call came back.

At the end of the completion block, I added a method call to updatePinCallout, which pretends that the user location dot was tapped again and reloads the address.

- (void)updatePinCallout
{    
  if (self.userLocationPin.selected) {
   [[NSOperationQueue mainQueue] addOperationWithBlock:^{
    [self.userLocationPin setSelected:YES animated:YES];
   }];
  }
}

Just remember to use the mainQueue to retap the dot or else the address won’t update on screen.

This change also fixed another bug that happened when the user location would jump when WIFI was enabled and the address label would not update to the new address.

Alternative Choice

Another thing that I found solved the problem pretty well was to zoom the map to the user location immediately, so that the animation didn’t even give a chance to tap the user location dot.

Other Bugs

Writing code for multiple scenarios unnecessarily

When I first implemented zooming to fit destinations, I only zoomed when the location was outside of the visible region. I realized that sometimes this would not be sufficient because sometimes the map would be zoomed way out, so I decided to account for that scenario too. I would zoom in if the destination was in the original visible area. This got complicated quickly.

Eventually, I realized that it was too much and just made it always zoom to the destination. The code went from 14 lines to 1.

Simpler is Better

Testing Days Later

I thought I was done with the project. Wrong. A few days later, I was trying out the app on the bus and came across two bugs.

  1. I was on a bus and the location was updating very quickly and constantly centering the map on the new location. The problem was that I wanted to look around the map and couldn’t do that without it changing while I was using it.
    • This is easily fixed by only zooming on the time the map loads.
  2. I was looking for a Pret A Manger. When I saw the results page, I found it utterly useless because all of them said the same thing and I couldn’t tell which location was where.
    • I think it would have been cool to add some directions as to how far each location was from my current location and in which direction or add the cross streets.

As the Pragmatic Programmer said:

“Test your software, or your users will.”

Where to next?

There’s certainly a lot of ways this can go. Add friends, add Facebook integration, add Yelp results, use Google Maps. That would be fun to do, but would lack direction.

For a product to be real, it must have a real use.

The way that this app fits into my life has something to do with lunch (ergo the name LunchTime). In an effort to build good habits, one of them is to walk 10K steps a day. As I’ve discovered, this means walking 1.3 hours a day. If I don’t plan to walk, I won’t get near 10K.

So over lunch, I like to walk 10 to 15 minutes away and back. I think it would be cool to draw a diamond around your current location to see how far you can walk in 5 and 15 minutes and show how far each location is.

Options abound. Looking forward to the next expedition.

It’s been fun working with MapKit. Time to play around with something else. Who knows? I might be looking at WatchKit, EventKit, or something else entirely.

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.