Playing With MapKit
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.
- Remove the jumpy effect on loading
- Move bar buttons from toolbar to buttons on the map
- Make the callout for the first search result come up
- Make the zip code and city on the same line
- Make the map center and zoom when the current location is tapped
- After clicking the textbox, resign the first responder when the map is clicked
- Add Directions
- *Zoom to include the destination in the Routeviewcontroller
- Intelligently choose walking vs driving directions
- 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.
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.
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];
4. Make the zip code and city on the same line
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.
5. Make the map center and zoom when the current location is tapped
Easy, call the zoomIn method that the zoom in button calls.
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]; }
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.
*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.
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.
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.
At the end of this process, I had an app that I could actually show people and it feels really good.