Overlay Images and Overlay Views with MapKit Tutorial

In this tutorial, you create an app for the Six Flags Magic Mountain amusement park using the latest version of MapKit. If you’re a roller coaster fan in the LA area, you’ll be sure to appreciate this app :] By Cesare Rocchi.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 6 of this article. Click here to view the first page.

I've Been Everywhere, Man - Switching The Map Type

In PVParkMapViewController.m, you will find a method at the bottom that looks like the following:

- (IBAction)mapTypeChanged:(id)sender {
    // TODO: Implement
}

Hmm, that's a pretty ominous-sounding TODO comment in there! :]

Fortunately, the starter project has much of what you'll need to flesh out this method. Did you note the UISegmentedControl sitting above the map view that seems to be doing a whole lot of nothing?

That UISegmentedControl is actually calling mapTypeChanged, but as you can see above, the method does nothing — yet!

Add the following code to mapTypeChanged method:

- (IBAction)mapTypeChanged:(id)sender {
    switch (self.mapTypeSegmentedControl.selectedSegmentIndex) {
        case 0:
            self.mapView.mapType = MKMapTypeStandard;
            break;
        case 1:
            self.mapView.mapType = MKMapTypeHybrid;
            break;
        case 2:
            self.mapView.mapType = MKMapTypeSatellite;
            break;
        default:
            break;
    }
}

Believe it or not, adding standard, satellite, and hybrid map types to your app is as simple as a switch statement on mapTypeSegmentedControl.selectedSegmentIndex, as seen in the code above! Wasn't that easy?

Build and run your app. Using the UISegmentedControl at the top of the screen, you should be able to flip through the map types, as seen below:

Park Satellite View

Even though the satellite view still is much better than the standard map view, it's still not very useful to your park visitors. There's nothing labelled — how will your users find anything in the park?

One obvious way is to drop a UIView on top of the MapView, but you can take it a step further and instead leverage the magic of MKOverlayRenderer to do a lot of the work for you!

What a View - All About Overlay Views

Note: If you're skipping ahead from earlier in this tutorial, you can pick up at this point with this starter project. Also, you should download the resources for this project which you'll be adding in as you go.

Before you start creating your own views, let's first talk about the classes that makes this all possible - MKOverlay and MKOverlayRenderer.

A MKOverlay is how you tell MapKit where you want the overlays drawn. There are three steps:

  1. Create your own custom class that implements the MKOverlay protocol, which has two required properties: coordinate and boundingMapRect. These two properties define where the overlay resides on the map, as well as its size.
  2. Create some instances of this class for every area you want to display an overlay for. For example, in this app, you might create one instance for a rollercoaster overlay, and one for a restaurant overlay.
  3. Finally, add the overlays to your Map View by calling this code:
[self.mapView addOverlay:overlay];

Now the MapView knows where it's supposed to display overlays - but how does it know what to display in each region?

Enter MKOverlayRenderer. You create a subclass of this to set up what you want to display in each spot. For example, in this app you'll just draw an image of the rollercoaster or restaurant.

A MKOverlayRenderer is really just a UIView in disguise, as it inherits from UIView. However, MKOverlayRenderer is a special kind of object that you don't add directly to the MKMapView. This is an object the MapKit framework expects you to provide. After you give it to MapKit, it will render it as an overlay on top of the map.

Remember how a MapView has a delegate - and you set it to your view controller in this tutorial? Well, there's a delegate method you implement to return an overlay view:

- (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id<MKOverlay>)overlay

This method is invoked when the map view realizes there is an MKOverlay object in the region that the map view's view-port is displaying.

To sum it up, you don't add MKOverlayRenderer objects directly to the map view; rather, you tell the map about MKOverlays to to display and return them when requested in the delegate method.

Now that you've covered the theory, it's time to put those concepts to use!

Put Yourself on the Map - Adding Your Own Information

As you saw earlier, the satellite view still doesn't provide enough information about the park. Your task is to create an object that represents an overlay for the entire park to dress it up a little.

Select the Overlays group and create a new class that derives from NSObject named PVParkMapOverlay. Then replace PVParkMapOverlay.h with the following:

#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>

@class PVPark;

@interface PVParkMapOverlay : NSObject <MKOverlay>

- (instancetype)initWithPark:(PVPark *)park;

@end

In the code above, you import the MapKit header, add the PVPark forward declaration, and then tell the compiler that this class conforms to the MKOverlay protocol. Finally, you define the method initWithPark.

Next, replace PVParkMapOverlay.m with the following:

#import "PVParkMapOverlay.h"
#import "PVPark.h"

@implementation PVParkMapOverlay

@synthesize coordinate;
@synthesize boundingMapRect;

- (instancetype)initWithPark:(PVPark *)park {
    self = [super init];
    if (self) {
        boundingMapRect = park.overlayBoundingMapRect;
        coordinate = park.midCoordinate;
    }
    
    return self;
}

@end

Above, you import the PVPark header. Then, since you are implementing the coordinate and boundingMapRect protocols that define properties, you must explicitly @synthesize them. Then implement the initWithPark method. This method simply takes the properties from the passed PVPark object, and sets them to the corresponding MKOverlay properties.

Now you need to create a view derived from the MKOverlayRenderer class.

Create a new class in the Overlays group called PVParkMapOverlayView that is a subclass of MKOverlayRenderer .

Add the following code to PVParkMapOverlayView.h, which defines a single method:

#import <MapKit/MapKit.h>

@interface PVParkMapOverlayView : MKOverlayRenderer

- (instancetype)initWithOverlay:(id<MKOverlay>)overlay overlayImage:(UIImage *)overlayImage;

@end

The implementation of PVParkMapOverlayView contains two methods, as well as a UIImage property in the class extension.

Next add the following code to PVParkMapOverlayView.m:

#import "PVParkMapOverlayView.h"

@interface PVParkMapOverlayView ()

@property (nonatomic, strong) UIImage *overlayImage;

@end

@implementation PVParkMapOverlayView

- (instancetype)initWithOverlay:(id<MKOverlay>)overlay overlayImage:(UIImage *)overlayImage {
    self = [super initWithOverlay:overlay];
    if (self) {
        _overlayImage = overlayImage;
    }
    
    return self;
}

- (void)drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context {
    CGImageRef imageReference = self.overlayImage.CGImage;
    
    MKMapRect theMapRect = self.overlay.boundingMapRect;
    CGRect theRect = [self rectForMapRect:theMapRect];
    
    CGContextScaleCTM(context, 1.0, -1.0);
    CGContextTranslateCTM(context, 0.0, -theRect.size.height);
    CGContextDrawImage(context, theRect, imageReference);
}

@end

Okay, here's a quick review of the code above.

initWithOverlay:overlayImage effectively overrides the base method initWithOverlay by providing a second argument overlayImage. The passed image is stored in the class extension property to be referenced in the next method,

- (void)drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context

This method is the real meat of this class; it defines how this view is rendered when given a specific MKMapRect, MKZoomScale, and the CGContextRef of the graphic context, with the intent to draw the overlay image onto the context at the appropriate scale.

Details on CoreGraphics drawing is quite far out of scope for this tutorial. However, you can see that the code above uses the passed MKMapRect to get a CGRect, in order to determine the location to draw the CGImageRef of the UIImage on the provided context. If you want to learn more about Core Graphics, check out our Core Graphics tutorial series.

Okay! Now that you have both an MKOverlay and MKOverlayRenderer, you can add them to your map view.

In PVParkMapViewController.m import both new classes:

#import "PVParkMapOverlayView.h"
#import "PVParkMapOverlay.h"

Next, add the code below to define a new method for adding an MKOverlay to the map view:

- (void)addOverlay {
    PVParkMapOverlay *overlay = [[PVParkMapOverlay alloc] initWithPark:self.park];
    [self.mapView addOverlay:overlay];
}

addOverlay should be called in loadSelectedOptions if the user has opted to show the Map Overlay.

Update loadSelectedOptions with the following code:

- (void)loadSelectedOptions {
    [self.mapView removeAnnotations:self.mapView.annotations];
    [self.mapView removeOverlays:self.mapView.overlays];
    for (NSNumber *option in self.selectedOptions) {
        switch ([option integerValue]) {
            case PVMapOverlay:
                [self addOverlay];
                break;
            default:
                break;
        }
    }
}

loadSelectedOptions is called every time the user dismisses the options selection view; it determines which options were selected and calls the appropriate methods to render those selections on the map view.

loadSelectedOptions also removes any annotations and overlays that may be present so that you don't end up with duplicate renderings. This is not necessarily efficient, but it is a simple approach for the purposes of this tutorial.

To implement the delegate method, add the code below (still in PVParkMapViewController.m):

- (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id<MKOverlay>)overlay {
    if ([overlay isKindOfClass:PVParkMapOverlay.class]) {
        UIImage *magicMountainImage = [UIImage imageNamed:@"overlay_park"];
        PVParkMapOverlayView *overlayView = [[PVParkMapOverlayView alloc] initWithOverlay:overlay overlayImage:magicMountainImage];
        
        return overlayView;
    }
    
    return nil;
}

When the MKOverlay is determined to be in view, the map view will call on PVParkMapViewController as the delegate to invoke this method. It expects that an MKOverlayRenderer will be returned for the matching MKOverlay.

In this case, you check to see if the overlay is of the class type PVParkMapOverlay; if so, the overlay image is loaded, a PVParkMapOverlayView instance is created with the overlay image, and then is returned to the caller.

There's one little piece missing, though — where does that suspicious little overlay_park come from?

That's a PNG file that was created to overlay the map view for the defined boundary of the park. The overlay_park image (from the resources for this tutorial) looks like this:

Add both the non-retina and retina images to your project under the Images group.

Build and run, choose the Map Overlay option, and voila! There's the park overlay drawn on top of your map, just like in the screenshot below:

Map Overlay

Zoom in, zoom out, and move around as much as you want — the overlay scales and moves as you would expect. Cool!

Contributors

Over 300 content creators. Join our team.