iOS Custom Annotation: a View Below the Annotation Pin

iOS Custom Annotation: A view below the annotation pin

You could just create your own annotation view:

@import MapKit;

@interface CustomAnnotationView : MKAnnotationView

@end

@interface CustomAnnotationView ()
@property (nonatomic) CGSize textSize;
@property (nonatomic) CGSize textBubbleSize;
@property (nonatomic, weak) UILabel *label;
@property (nonatomic) CGFloat lineWidth;
@property (nonatomic) CGFloat pinRadius;
@property (nonatomic) CGFloat pinHeight;

@property (nonatomic, strong) UIBezierPath *pinPath;
@property (nonatomic, strong) UIBezierPath *textBubblePath;
@end

@implementation CustomAnnotationView

- (instancetype)initWithAnnotation:(id<MKAnnotation>)annotation reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier];
if (self) {
self.lineWidth = 1.0;
self.pinHeight = 40;
self.pinRadius = 15;

UILabel *label = [[UILabel alloc] init];
label.textAlignment = NSTextAlignmentCenter;
label.font = [UIFont preferredFontForTextStyle:UIFontTextStyleCallout];
label.textColor = [UIColor whiteColor];
[self addSubview:label];
self.label = label;

[self adjustLabelWidth:annotation];

self.opaque = false;
}
return self;
}

- (void)setAnnotation:(id<MKAnnotation>)annotation {
[super setAnnotation:annotation];
if (annotation) [self adjustLabelWidth:annotation];
}

- (void)adjustLabelWidth:(id<MKAnnotation>)annotation {
NSString *title = [annotation title];
NSDictionary *attributes = @{NSFontAttributeName : self.label.font};
self.textSize = [title sizeWithAttributes:attributes];
CGFloat delta = self.textSize.height * (1.0 - sinf(M_PI_4)) * 0.55;
self.textBubbleSize = CGSizeMake(self.textSize.width + delta * 2, self.textSize.height + delta * 2);
self.label.frame = CGRectMake(0, self.pinHeight, self.textBubbleSize.width, self.textBubbleSize.height);
self.label.text = title;
self.frame = CGRectMake(0, 0, self.textBubbleSize.width, self.pinHeight + self.textBubbleSize.height);
self.centerOffset = CGPointMake(0, self.frame.size.height / 2.0 - self.pinHeight);
}

- (void)drawRect:(CGRect)rect {
CGFloat radius = self.pinRadius - self.lineWidth / 2.0;
CGPoint startPoint = CGPointMake(self.textBubbleSize.width / 2.0, self.pinHeight);
CGPoint center = CGPointMake(self.textBubbleSize.width / 2, self.pinRadius);
CGPoint nextPoint;

// pin

self.pinPath = [UIBezierPath bezierPath];
[self.pinPath moveToPoint:startPoint];
nextPoint = CGPointMake(self.textBubbleSize.width / 2 - radius, self.pinRadius);
[self.pinPath addCurveToPoint:nextPoint
controlPoint1:CGPointMake(startPoint.x, startPoint.y - (startPoint.y - nextPoint.y) / 2.0)
controlPoint2:CGPointMake(nextPoint.x, nextPoint.y + (startPoint.y - nextPoint.y) / 2.0)];

[self.pinPath addArcWithCenter:center radius:radius startAngle:M_PI endAngle:0 clockwise:TRUE];
nextPoint = startPoint;
startPoint = self.pinPath.currentPoint;
[self.pinPath addCurveToPoint:nextPoint
controlPoint1:CGPointMake(startPoint.x, startPoint.y - (startPoint.y - nextPoint.y) / 2.0)
controlPoint2:CGPointMake(nextPoint.x, nextPoint.y + (startPoint.y - nextPoint.y) / 2.0)];
[[UIColor blackColor] setStroke];
[[UIColor colorWithRed:0.0 green:0.5 blue:1.0 alpha:0.8] setFill];
self.pinPath.lineWidth = self.lineWidth;
[self.pinPath fill];
[self.pinPath stroke];
[self.pinPath closePath];

// bubble around label

if ([self.annotation.title length] > 0) {
self.textBubblePath = [UIBezierPath bezierPath];
CGRect bubbleRect = CGRectInset(CGRectMake(0, self.pinHeight, self.textBubbleSize.width, self.textBubbleSize.height), self.lineWidth / 2, self.lineWidth / 2);
self.textBubblePath = [UIBezierPath bezierPathWithRoundedRect:bubbleRect
cornerRadius:bubbleRect.size.height / 2];
self.textBubblePath.lineWidth = self.lineWidth;
[self.textBubblePath fill];
[self.textBubblePath stroke];
} else {
self.textBubblePath = nil;
}

// center white dot

self.pinPath = [UIBezierPath bezierPathWithArcCenter:center radius:radius / 3.0 startAngle:0 endAngle:M_PI * 2.0 clockwise:TRUE];
self.pinPath.lineWidth = self.lineWidth;
[[UIColor whiteColor] setFill];
[self.pinPath fill];
}

- (UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event {
if ([self.pinPath containsPoint:point] || [self.textBubblePath containsPoint:point])
return self;

return nil;
}

@end

That yields something like:

Sample Image

Clearly, you can customize this to your heart's content, but it illustrates the basic idea: Write a MKAnnotationView subclass that overrides initWithAnnotation:reuseIdentifier: and implement your own drawRect.

MKMapView: Instead of Annotation Pin, a custom view

When you want to use your own image for an annotation view, you should create an MKAnnotationView instead of an MKPinAnnotationView.

MKPinAnnotationView is a subclass of MKAnnotationView so it has an image property but it generally overrides that and draws a pin image (that's what it's for).

So change the code to:

-(MKAnnotationView *)mapView:(MKMapView *)mV viewForAnnotation:(id <MKAnnotation>)annotation 
{
MKAnnotationView *pinView = nil;
if(annotation != mapView.userLocation)
{
static NSString *defaultPinID = @"com.invasivecode.pin";
pinView = (MKAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:defaultPinID];
if ( pinView == nil )
pinView = [[MKAnnotationView alloc]
initWithAnnotation:annotation reuseIdentifier:defaultPinID];

//pinView.pinColor = MKPinAnnotationColorGreen;
pinView.canShowCallout = YES;
//pinView.animatesDrop = YES;
pinView.image = [UIImage imageNamed:@"pinks.jpg"]; //as suggested by Squatch
}
else {
[mapView.userLocation setTitle:@"I am here"];
}
return pinView;
}


Notice that animatesDrop is also commented out since that property only exists in MKPinAnnotationView.

If you still want your image annotations to drop, you'll have to do the animation yourself. You can search Stack Overflow for "animatesdrop mkannotationview" and you'll find several answers. Here are the first two:

  • Is it possible to call animatesDrop in a MKAnnotationView rather than MKPinAnnotationView?
  • How can I create a custom "pin-drop" animation using MKAnnotationView?

Adding a title in front/below a custom annotation mapkit swift 4

You can create a label like so:

let annotationLabel = UILabel(frame: CGRect(x: -40, y: -35, width: 105, height: 30))
annotationLabel.numberOfLines = 3
annotationLabel.textAlignment = .center
annotationLabel.font = UIFont(name: "Rockwell", size: 10)
// you can customize it anyway you want like any other label

Set the text:

annotationLabel.text = annotation.title!!

And then add to annotation view:

annotationView.addSubview(annotationLabel)

Picture of annotation with label

I also added a background and border by doing:

annotationLabel.backgroundColor = UIColor.white
annotationLabel.layer.cornerRadius = 15
annotationLabel.clipsToBounds = true

You can also change where the label is in respect to the annotation by changing the X and Y when creating the label. Negative is to the left and up, positive right and down.

Custom Annotation for Mapview Swift

There's nothing seriously wrong with the code. But there can be a couple of things that would cause problems, including:

  1. Have you set the delegate (either in IB or programmatically) for the map view? If not, your mapView(_:viewFor:) will never be called. Add breakpoint or debugging print statement to confirm.

  2. Have you confirmed that UIImage(named: "Skyscraper") is successfully retrieving an image? Make sure this is not returning nil.


Note, if only iOS 11 and later, you can simplify this code a bit. Since iOS 11, we no longer need for mapView(_:viewFor:) in simple scenarios like this. I would suggest putting the annotation view configuration code within the annotation view subclass, where it belongs, and avoid cluttering our view controller with a viewFor implementation.

So when you do get the current issue behind you, the recommended process is:

  1. Define classes for your annotation and annotation view:

    class CustomAnnotation: MKPointAnnotation {
    var pinCustomImage: UIImage!
    }

    And

    class CustomAnnotationView: MKAnnotationView {
    override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
    super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
    canShowCallout = true
    update(for: annotation)
    }

    override var annotation: MKAnnotation? { didSet { update(for: annotation) } }

    required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
    }

    private func update(for annotation: MKAnnotation?) {
    image = (annotation as? CustomAnnotation)?.pinCustomImage
    }
    }
  2. In viewDidLoad register this annotation view class:

    mapView.register(CustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
  3. Remove mapView(_:viewFor:) implementation.

Now when you add a CustomAnnotation to your map’s list of annotations, it will be rendered correctly.

But I would suggest resolving your current problem first. There’s no point in refining you implementation until these more basic issues are resolved.



Related Topics



Leave a reply



Submit