Displaying Custom Pins on MKMapView for iOS

Update: Please note this post is not up to date and today I no longer recommend this approach for customizing pins for iOS Maps. Apple now makes it much easier to customize map pins, please read this.


Please forgive minor inaccuracies since this is my very first iOS development post! email me with your suggestions. Also, this should work with at least iOS 7 and 8.

IMG_0059.jpg

I have found a few articles on the web that explain how to customize map pins but it turns out that a few issues arise when one does. Additionally an important use-case that I haven’t read a lot about is when pins need to contain dynamic elements such as text, which would be unique to one pin. I thought that I’d consolidate all this information in one post.

First, when designing custom UI elements I always prefer implementing a separate class. Therefore I recommend sub-classing either MKAnnotationView or MKPinAnnotationView in order to represent a pin. Just as a note, MKPinAnnotationView is a child of MKAnnotationView that allows customizing the color of the pin if using Apple’s default pins, as well as choosing whether or not they animate. Since we are going to override all the defaults, it really makes no difference. Additionally it seems that custom pins won’t animate if MKPinAnnotationView.animatesDrop = YES, instead you’ll lose your custom design and get the default pin.

In this little project, I’m going to assume we want to display pins that have a price label on them. We’re also going to need at least 3 classes, one to represent the custom pin view, one to represent the point on the map and one for the UIViewController that contains the MKMapView element.

Point Annotation #

First, let’s write the code to represent the point on the map. This is just a point and doesn’t define anything visual. This is a good place to store data associated with your point. In our case we want to store the price value. When MKMapView needs to render a pin, we’ll have access to the point object where we can fetch the data we need to define the appropriate visuals.

Here’s my code for MyCustomPointAnnotation.h

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

@interface MyCustomPointAnnotation : MKPointAnnotation

@property float price;

@end

And for MyCustomPointAnnotation.m

#import "MyCustomPointAnnotation.h"

@implementation MyCustomPointAnnotation

@end

I’m really doing nothing here except store the price.

Pin Annotation View #

Here’s how to get started for MyCustomPinAnnotationView.h

#import <MapKit/MapKit.h>

@interface MyCustomPinAnnotationView : MKAnnotationView

@property float price;

- (instancetype)initWithAnnotation:(id<MKAnnotation>)annotation
                   price:(float)price;

@end

It is not necessary to declare an initializer method because the super class already does, however I personally prefer to.
Below is the code we’ll start with for MyCustomPinAnnotationView.m

#import "MyCustomPinAnnotationView.h"

@implementation MyCustomPinAnnotationView

- (instancetype)initWithAnnotation:(id<MKAnnotation>)annotation
                   price:(float)price
{
    // The re-use identifier is always nil because these custom pins may be visually different from one another
    self = [super initWithAnnotation:annotation 
                     reuseIdentifier:nil];

    self.price = price;

    // Callout settings - if you want a callout bubble
    self.canShowCallout = YES;
    self.rightCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];

    return self;
}

In order to replace the pin with a custom image, use the follow line:

self.image = [UIImage imageNamed:@"myPinImage"];

You can add a label with the following code:

UILabel* label = [[UILabel alloc] initWithFrame:CGRectMake(/* x */, /* y */, /* width */, /* height */)];
label.textAlignment = NSTextAlignmentCenter;
label.textColor = [UIColor blackColor];
label.text = [NSString stringWithFormat:@"%f", self.price];
label.font = [label.font fontWithSize:10];
[self addSubview:label];

It may take some trial and error to figure out the right x/y position and size of the label. You can also base them off of your image’s dimensions with self.image.size.width and self.image.size.height.

If you are using callout bubbles, they may appear offset because your image isn’t the same size as Apple’s standard pin. This can be fixed with: self.calloutOffset = CGPointMake(/* x */, /* y */);

Before you can move on to the next step, there is one last issue. Your custom pin will not react to touch events. In order to fix that, override a couple super methods by adding this at the end of MyCustomPinAnnotationView.m:

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
    UIView* hitView = [super hitTest:point withEvent:event];
    if (hitView != nil)
    {
        [self.superview bringSubviewToFront:self];
    }
    return hitView;
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event
{
    CGRect rect = self.bounds;
    BOOL isInside = CGRectContainsPoint(rect, point);
    if(!isInside)
    {
        for (UIView *view in self.subviews)
        {
            isInside = CGRectContainsPoint(view.frame, point);
            if(isInside)
                break;
        }
    }
    return isInside;
}

UIViewController #

Now is time to ensure our map uses this custom pin so we can check everything works. Ensure that the UIViewController that contains the map subscribes to the MKMapViewDelegate protocol with <MKMapViewDelegate> in the header file. Also don’t forget to explicitly set the delegate in the implementation file:

- (void)viewDidLoad
{
    [super viewDidLoad];
    [MapView setDelegate:self];
}

In order to display custom pins on a map, two things must be done in the UIViewController. First the pins must be added to the map element and the appropriate MKMapViewDelegate protocol methods must be implemented. Anywhere appropriate, on viewDidLoad for example or elsewhere, add the pins to your map:

MyCustomPointAnnotation* point = [[MyCustomPointAnnotation alloc] init];
point.coordinate = CLLocationCoordinate2DMake(/* latitude */, /* longitude */);
point.price = /* price */;
[MapView addAnnotation:point];

Finally for everything to work, implement the mapView:viewForAnnotation: method as follows:

- (MKAnnotationView*)mapView:(MKMapView *)mapView
           viewForAnnotation:(id<MKAnnotation>)annotation
{
    // Don't do anything if it's the user's location point
    if([annotation isKindOfClass:[MKUserLocation class]]) return nil;

    // Fetch all necessary data from the point object
    float price = ((MyCustomPointAnnotation*)annotation).price;

    MyCustomPinAnnotationView* pin = 
    [[MyCustomPinAnnotationView alloc]initWithAnnotation:annotation
                                               price:price];
    return pin;
}

If you have implemented this method before, you may notice that we’re not defining a “re-use identifier”. The reason for this is that these custom pins are going to be different from one another from a visual stand-point, since they’ll have custom labels.

All the code can be found in a sample project I’ve uploaded to GitHub: https://github.com/tlextrait/MyCustomPinProject

 
369
Kudos
 
369
Kudos

Now read this

Early Words - iOS App

I’ve just released Early Words to the iOS AppStore. You can find the app right here. This is the first commercially viable mobile app I’ve built entirely on my own from A to Z, for my own business (LycheeApps Inc.), and without external... Continue →