A Curriculum of Guides about Blocks in Objective-C

Recently, there was a question about blocks that I couldn’t answer so I took a note to review it.

I came upon some great articles. Seeing the same elephant described by different people invariably leads to a comprehensive understanding of the elephant (much in the same way that taking beginner swing dance classes from 4-5 people leads to better fundamentals).

With no desire to write an inferior blog post, I’m going to review the ones I found, much the same way that I review restaurants.

The Guides

Mike Nachbaur: Using GCD and Blocks Effectively

Tutsplus: Objective-C Succinctly: Blocks

AppCoda: Introduction to Objective-C Blocks

Apple Working with Blocks

Matt Nunogawa Objective C Blocks: Summary, Syntax & Best Practices

Conrad Stoll Blocks, Operations, and Retain Cycles

I’ll be looking at:

  • explanation of why it’s important
  • completeness vs depth
  • the example code
  • ease of transferring examples to projects
  • how beginner friendly it is

Which should I read first?

Starting from Square One

Go with Tutsplus: Objective-C Succinctly: Blocks. It’s short, has useful examples and memory is well explained with diagrams. It stops short of retain loops though, which is a vital topic. Also lacking is why blocks are used (when would I want to use a block? what would I have used otherwise?).

AppCoda (Introduction to Objective-C Blocks) is really known for their app tutorials. They explain very well why you would use blocks and lead you through an example of how it would be used in a practical situation with an actual app.

Mike Nachbaur: Using GCD and Blocks Effectively extends deeper into asynchronous callbacks, introduces Grand Central Dispatch, and the idea that retain cycles can happen.

For Programmers from Other Languages

Matt Nunogawa (Objective C Blocks: Summary, Syntax & Best Practices) writes his blog directed at programmers from other languages and overviews blocks with greater examples for retain cycles and how to avoid them.

Bonus: Conrad Stoll (Blocks, Operations, and Retain Cycles) writes about a real example of a bug that his team faced with retain cycles and blocks.

For Quick Reference After Learning It Once

Apple Working with Blocks documentation can be a bit dry and doesn’t give much guidance for when to use it, but if that doesn’t bother you, then by all means go ahead. It’s very comprehensive.

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.