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 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
%d bloggers like this: