Size an MKMapView to Fit its Annotations in iOS Without Futzing with Coordinate Systems

map viewAn oddity of MKMapView is that it always defaults to showing the western hemisphere of the Earth. That’s almost certainly not what you want. Instead, you have probably set an array of id <MKAnnotation> objects which you want to appear as pins on the map. If the pins were evenly distributed over the USA, then great. Otherwise, the MapView is not at all zoomed the way you want. It baffles me a map view doesn’t just zoom itself by default when you set annotations, but it doesn’t.

One approach might be to loop over all of your annotations and figure out what the max and min x and y values are across them all. Then get the center point of the map view and do some math to convert the coordinates and scale the map view.

Alternatively, MapKit has API that is separate from the map view itself for manipulating rectangles and regions which can do the job automatically.

The main problem is that id <MKAnnotation> objects contain a CoreLocation coordinate – CLLocationCoordinate2D – which represents a GPS coordinate while MapView has an MKCoordinateRegion which has a CLLocationCoordinate2D defining its center but uses an MKCoordinateRegion span struct to define the zoom level of the map view in terms of degrees of arc.

MapKit provides a mix of object-oriented API and C functions to convert an array of CLLocationCoordinate2D structs from id <MKAnnotation> objects into an MKCoordinateRegion suitable for setting the region in a MKMapView. These transformations are straightforward and don’t require fiddling with converting between coordinate systems.

Here is the code in a map view controller

#define MINIMUM_ZOOM_ARC 0.014 //approximately 1 miles (1 degree of arc ~= 69 miles)
#define ANNOTATION_REGION_PAD_FACTOR 1.15
#define MAX_DEGREES_ARC 360
//size the mapView region to fit its annotations
- (void)zoomMapViewToFitAnnotations:(MKMapView *)mapView animated:(BOOL)animated
{ 
    NSArray *annotations = mapView.annotations;
    int count = [mapView.annotations count];
    if ( count == 0) { return; } //bail if no annotations
    
    //convert NSArray of id <MKAnnotation> into an MKCoordinateRegion that can be used to set the map size
    //can't use NSArray with MKMapPoint because MKMapPoint is not an id
    MKMapPoint points[count]; //C array of MKMapPoint struct
    for( int i=0; i<count; i++ ) //load points C array by converting coordinates to points
    {
        CLLocationCoordinate2D coordinate = [(id <MKAnnotation>)[annotations objectAtIndex:i] coordinate];
        points[i] = MKMapPointForCoordinate(coordinate);
    }
    //create MKMapRect from array of MKMapPoint
    MKMapRect mapRect = [[MKPolygon polygonWithPoints:points count:count] boundingMapRect];
    //convert MKCoordinateRegion from MKMapRect
    MKCoordinateRegion region = MKCoordinateRegionForMapRect(mapRect);
    
    //add padding so pins aren't scrunched on the edges
    region.span.latitudeDelta  *= ANNOTATION_REGION_PAD_FACTOR;
    region.span.longitudeDelta *= ANNOTATION_REGION_PAD_FACTOR;
    //but padding can't be bigger than the world
    if( region.span.latitudeDelta > MAX_DEGREES_ARC ) { region.span.latitudeDelta  = MAX_DEGREES_ARC; }
    if( region.span.longitudeDelta > MAX_DEGREES_ARC ){ region.span.longitudeDelta = MAX_DEGREES_ARC; }
    
    //and don't zoom in stupid-close on small samples
    if( region.span.latitudeDelta  < MINIMUM_ZOOM_ARC ) { region.span.latitudeDelta  = MINIMUM_ZOOM_ARC; }
    if( region.span.longitudeDelta < MINIMUM_ZOOM_ARC ) { region.span.longitudeDelta = MINIMUM_ZOOM_ARC; }
    //and if there is a sample of 1 we want the max zoom-in instead of max zoom-out
    if( count == 1 )
    { 
        region.span.latitudeDelta = MINIMUM_ZOOM_ARC;
        region.span.longitudeDelta = MINIMUM_ZOOM_ARC;
    }
    [mapView setRegion:region animated:animated];
}

- (void)viewWillAppear:(BOOL)animated
{
    [self zoomMapViewToFitAnnotations:self.mapView animated:animated];
    //or maybe you would do the call above in the code path that sets the annotations array
}

How to Create an Xcode 4.0-style Window-based Application in Xcode 4.2

I’ve decided to get up to speed on iOS programming and, to help me with that, I bought Aaron Hillegass’s iOS Programming: The Big Nerd Ranch Guid 2nd edition. I’ve listened to Aaron on podcasts and have heard that his training method is fantastic. The book does seem to be unusually well written for a tech book but the problem I immediately ran into is that it was written for Xcode 4.0 and iOS 4.3 SDK. I have Lion with Xcode 4.2 and iOS 5 SDK. That’s part of the adventure of discovery and change in tech but unfortunately Apple removed the project type that Hillegass’s examples are based on which is a major stumbling block.

Hillegass uses the “Window-based Application” template for iOS as the basis for all of his projects. That template no longer exists in Xcode. The closest thing in Xcode 4.2 is “Empty Application”. However, that’s not quite the same thing because the now-defunct “Window-based Application” template generated a main window XIB view wired to generated controller but the “Empty Application” has no XIB. In order to make any sense out of the book, you have to create the XIB and wire it up yourself. This is no big deal to anybody with a little experience but to a newb like me, it took a bit of wandering in the wilderness at the Big Nerd Ranch forums to figure out how to manually get the project into the state that the exercises in the book will work.

How to Convert a new “Empty Application” into a “Window-based Application”

Start by creating a new iOS Application project using the “Empty Application” project. This will create a very similar project to the “Window-based Application” project described in iOS Programming 2/e with just a few differences:

  1. The main controller class that gets generated is “AppDelegate” rather than “<project-name>AppDelegate”. The
  2. There is no XIB view.
  3. The window property of AppDelegate doesn’t have the IBOutlet macro to make it visible to the Interface Builder tooling.
    1. Prepare the controller to talk to the Interface Builder tooling

    In the controller class interface definition of the we need to add the IBOutlet macro so that the tooling will recognize the controller. In your controller header file, AppDelegate.h, the line to modify begins with @property and ends with *window. Add IBOutlet in front of the UIWindow type as shown below.

#import <UIKit/UIKit.h>

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) IBOutlet UIWindow *window;

@end

Save this change.

Update the didFinishLaunchingWithOptions: method

The auto-generated first line of code in the didFinishLaunchingWithOptions: method of AppDelegate.m prevents message routing from working properly. Comment it out as below:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    //self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];
    // Override point for customization after application launch.
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    return YES;
}

Save this change.

Create the XIB file

Right-click on the folder containing your AppDelegate files and choose “Add File”. Select “Window” from the iOS User Interface group. Choose the appropriate device family—for the initial Quiz project that is iPhone. The default file name is Window.xib which works fine or you can name it MainWindow.xib to make it just like in the book.

OS X 10.7 (Lion)-2011-11-23-11-18-56

Change the Class of the XIB file’s owner to UIApplication

Select Window.xib in the Navigator. Select the “File’s Owner” cube in the objects pane just left of the layout canvas and change the Class in the properties pane to UIApplication.

OS X 10.7 (Lion)-2011-11-23-11-34-47

 

Add an object for the controller

Filter the tool pallet on the bottom right to “Objects and Controllers” and drag and drop an Object from the toolbox onto the object pane.

OS X 10.7 (Lion)-2011-11-23-11-37-52

Set the class of the controller object

In the properties window, change the class to controller that was generated for your project by the template. That should be AppDelegate. The Object will automatically be renamed with a space added into the Pascal casing. (You can override this by putting something into the Label field of the Identity pane.)

OS X 10.7 (Lion)-2011-11-23-11-45-00

Route the XIB delegate to the controller

Now we want to wire up the MVC message routing for the view. Select the “File’s Owner” cube in the object pane and control+drag the delegate outlet to the App Delegate object.

OS X 10.7 (Lion)-2011-11-23-12-00-15

 

Route the controller object’s window outlet to the window object

Next perform a similar ctrl+drag of the window outlet from the App Delegate object to the Window object.

OS X 10.7 (Lion)-2011-11-23-12-01-08

All done

Now our Empty Application project is set up just like a Window-based Application as described in iOS Programming 2nd Edition. The file names may be a bit different but that should have no practical  effect. The AppDelegate inherits from UIResponder <UIApplicationDelegate> rather than NSObject<UIApplicationDelegate>. This doesn’t seem to be a problem, either.

Now that I have figured out how to create an wire up a main window XIB file, everything seems to work great as I follow along in the book.

%d bloggers like this: