Draw Mkpointannotation with Title in Mksnapshot Image

Draw MKPointAnnotation with title in MKSnapshot image

You can use the following steps:

  • with MKMapSnapshotter you will get an image of the map without annotations

  • you can retrieve the annotations from your MKMapView

  • for each annotations determine its position in the coordinate space of the image

  • draw a custom pin there (may look like Apple's pins)

  • determine the text and size of the annotation title and draw it centered below the position of the pin

The result can look very similar to what MKMapView displays. In the attached screenshot there is a MKMapView in the upper area and an UIImageView with the resulting image in the lower area. Looks similar, doesn't it?

Sample Image

Here the Swift 4 code for the screenshot above:

    @IBOutlet weak var imageView: UIImageView!

@IBAction func onSnap(_ sender: Any) {
let options: MKMapSnapshotOptions = MKMapSnapshotOptions()
options.region = self.mapView.region
options.size = self.mapView.frame.size
options.scale = UIScreen.main.scale

let customPin = UIImage(named: "customPin.pdf")

let snapshotter = MKMapSnapshotter(options: options)
snapshotter.start { [weak self] (snapshot: MKMapSnapshot?, error: Error?) -> Void in
guard error == nil, let snapshot = snapshot else { return }

UIGraphicsBeginImageContextWithOptions(snapshot.image.size, true, snapshot.image.scale)
snapshot.image.draw(at: CGPoint.zero)

let titleAttributes = self?.titleAttributes()
for annotation in (self?.mapView.annotations)! {
let point: CGPoint = snapshot.point(for: annotation.coordinate)
if let customPin = customPin {
self?.drawPin(point: point, customPin: customPin)
}
if let title = annotation.title as? String {
self?.drawTitle(title: title,
at: point,
attributes: titleAttributes!)
}
}
let compositeImage = UIGraphicsGetImageFromCurrentImageContext()
self?.imageView.image = compositeImage
}
}

private func drawTitle(title: String,
at point: CGPoint,
attributes: [NSAttributedStringKey: NSObject]) {
let titleSize = title.size(withAttributes: attributes)
title.draw(with: CGRect(
x: point.x - titleSize.width / 2.0,
y: point.y + 1,
width: titleSize.width,
height: titleSize.height),
options: .usesLineFragmentOrigin,
attributes: attributes,
context: nil)
}

private func titleAttributes() -> [NSAttributedStringKey: NSObject] {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
let titleFont = UIFont.systemFont(ofSize: 10, weight: UIFont.Weight.semibold)
let attrs = [NSAttributedStringKey.font: titleFont,
NSAttributedStringKey.paragraphStyle: paragraphStyle]
return attrs
}

private func drawPin(point: CGPoint, customPin: UIImage) {
let pinPoint = CGPoint(
x: point.x - customPin.size.width / 2.0,
y: point.y - customPin.size.height)
customPin.draw(at: pinPoint)
}
}

Alternative

If you prefer to draw a MKMarkerAnnotationView (e.g. to get the nice shadow for free) you can change the drawPin to this:

private func drawPin(point: CGPoint, annotation: MKAnnotation) {
let annotationView = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "test")
annotationView.contentMode = .scaleAspectFit
annotationView.bounds = CGRect(x: 0, y: 0, width: 40, height: 40)
annotationView.drawHierarchy(in: CGRect(
x: point.x - annotationView.bounds.size.width / 2.0,
y: point.y - annotationView.bounds.size.height,
width: annotationView.bounds.width,
height: annotationView.bounds.height),
afterScreenUpdates: true)
}

Don't forget to change the call to

self?.drawPin(point: point, annotation: annotation)

The result looks like this then:

Sample Image

iOS Custom annotations for MKMapView

A quite late response, but I guess other people would still be interested

In a MKAnnotationView subclass:

First, define some dimensions:

#define ckImageHeight 65
#define ckImageWidth 55
#define kBorder 5

Then define the Annotation View's frame:

self.frame = CGRectMake(0, 0, ckImageWidth, ckImageHeight);

If you want the background to be an image and not just a color:

self.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"map_checkin"]];

Then make a placeholder for the image

CGRect checkInPictureRect = CGRectMake(kBorder, kBorder, ckImageWidth - 9 , ckImageWidth - 9);
UIView *checkInPictureView = [[UIView alloc]initWithFrame:checkInPictureRect];

Then the fun starts:

// Crop image
UIImage *croppedImage = [ImageHelper centerCropImage:image];

// Resize image
CGSize checkInPictureSize = CGSizeMake(checkInPictureRect.size.width*1.5, checkInPictureRect.size.height*1.5);
UIGraphicsBeginImageContext(checkInPictureSize);
[croppedImage drawInRect:CGRectMake(0, 0, checkInPictureRect.size.width*1.5, checkInPictureRect.size.height*1.5)];
UIImage* resizedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

UIImageView *imageView = [[UIImageView alloc] initWithImage:resizedImage];
imageView.frame = checkInPictureView.bounds;
[checkInPictureView addSubview:imageView];
[self addSubview:checkInPictureView];

// Counter
UIView *counterView = [[UIView alloc]initWithFrame:CGRectMake(45, -2, 15, 15)];
counterView.opaque = YES;
(checkIn.isNow) ? [counterView setBackgroundColor:[UIColor enloopBlue]] : [counterView setBackgroundColor:[UIColor enloopGreen]];
counterView.layer.cornerRadius = 8;

self.counterLabel = [[UILabel alloc] init];
self.counterLabel.frame = CGRectMake(4, 2, 10, 10);

if (self.count >= 10) {
counterView.frame = CGRectMake(45, -2, 18, 18);
self.counterLabel.frame = CGRectMake(3, 3, 12, 12);
}

[self.counterLabel setTextColor:[UIColor whiteColor]];
[self.counterLabel setFont:[UIFont fontWithName: @"Trebuchet MS" size: 11.0f]];
[self.counterLabel setText:[[NSString alloc] initWithFormat:@"%lu", (unsigned long)self.count]];
[counterView addSubview:self.counterLabel];
[counterView bringSubviewToFront:self.counterLabel];
[self addSubview:counterView];

As for the centerCropImage helper, nothing special:

+ (UIImage *)centerCropImage:(UIImage *)image {
// Use smallest side length as crop square length
CGFloat squareLength = MIN(image.size.width, image.size.height);
// Center the crop area
CGRect clippedRect = CGRectMake((image.size.width - squareLength) / 2, (image.size.height - squareLength) / 2, squareLength, squareLength);

CGImageRef imageRef = CGImageCreateWithImageInRect([image CGImage], clippedRect);
UIImage * croppedImage = [UIImage imageWithCGImage:imageRef];
CGImageRelease(imageRef);
return croppedImage;
}

I know there are quite a few things to improve, but untill then I hope it will help others. :)

How to link an annotation in MapView to its calling pin

You need to use this method func mapView(_ mapView: MKMapView,annotationView view: MKAnnotationView,calloutAccessoryControlTapped control: UIControl) of MKMapViewDelegate

Something like this

func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView,
calloutAccessoryControlTapped control: UIControl) {

//Here you have the annotation that was selected
let selectedAnnotation = view.annotation
//Do whatever you need here
}

How to update information on MKPinAnnotationView?

If you want the annotation view to show some text that is updated as the annotation changes, use KVO on the annotation. So, first, create a model object, the annotation, which includes the new property to be observed:

class MyAnnotation: NSObject, MKAnnotation {
dynamic var title: String?
dynamic var subtitle: String?
dynamic var coordinate: CLLocationCoordinate2D
dynamic var information: String?

init(title: String? = nil, subtitle: String? = nil, coordinate: CLLocationCoordinate2D, information: String? = nil) {
self.title = title
self.subtitle = subtitle
self.coordinate = coordinate
self.information = information
}
}

I've called this new property information, but you can probably come up with a better name that captures the functional intent of this property. But hopefully it illustrates the idea. The key takeaway here is that if this property may change at a later point, we'll want to make it dynamic so we can use KVO to observe those changes.

class MyPinAnnotationView: MKPinAnnotationView {
private let informationLabel = UILabel(frame: CGRect(origin: .zero, size: CGSize(width: 70.0, height: 30.0)))

private var observerContext = 0

override var annotation: MKAnnotation? {
willSet {
removeObserverIfAny()
}
didSet {
if let annotation = annotation as? MyAnnotation {
annotation.addObserver(self, forKeyPath: #keyPath(MyAnnotation.information), context: &observerContext)
informationLabel.text = annotation.information
}
}
}

deinit {
removeObserverIfAny()
}

private func removeObserverIfAny() {
if let oldAnnotation = annotation as? MyAnnotation {
oldAnnotation.removeObserver(self, forKeyPath: #keyPath(MyAnnotation.information))
}
}

func showInformation() {
addSubview(informationLabel)
}

func hideInformation() {
informationLabel.removeFromSuperview()
}

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard context == &observerContext else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
return
}

if let annotation = annotation as? MyAnnotation, let information = annotation.information {
informationLabel.text = information
}
}

}

extension ViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation { return nil }

var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MyPinAnnotationView
if annotationView == nil {
annotationView = MyPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
annotationView?.canShowCallout = true
} else {
annotationView?.annotation = annotation
}
annotationView?.showInformation()
return annotationView
}
}

I've changed the name of the label to informationLabel to make it a little more explicit that it is a view, not to be confused with the model property, information, of our new MyAnnotation class. Also, I'd suggest more meaningful class names than MyAnnotation and MyPinAnnotationView, perhaps using some name that better captures the functional intent of these two classes.

Regardless, as you can see, when you set the annotation for this annotation view, it updates the label text. But it also observes the annotation's new information property via KVO, so if this property changes later, the view will update accordingly.

Swift - setting different images from array to annotation pins

First, you need to create your own class that adopts the MKAnnotation protocol for your annotations -

class RestaurantAnnotation : NSObject, MKAnnotation {
var coordinate: CLLocationCoordinate2D
var title: String
var subtitle: String
var image: UIImage?

init(coordinate: CLLocationCoordinate2D, title: String, subtitle: String) {
self.coordinate = coordinate
self.title = title
self.subtitle = subtitle
}
}

Then, use instances of this class when you add the annotation and set the image -

for var r=0;r<arrayOfRestaurants.count;r++
{

var summonRestaurant:NSDictionary = arrayOfRestaurants[r] as NSDictionary
var nearbyRestaurant = Restaurant(nearbyRestaurants:summonRestaurant)
var latRestaurant=(nearbyRestaurant.latitude as NSString).doubleValue
var longRestaurant=(nearbyRestaurant.longitude as NSString).doubleValue
let locationOfRestaurant = CLLocationCoordinate2D(
latitude: latRestaurant as CLLocationDegrees, longitude: longRestaurant as CLLocationDegrees)
var lunchArray: NSArray = nearbyRestaurant.lunch as NSArray

let title = nearbyRestaurant.name + " " + nearbyRestaurant.distance +" km"
var annotation = RestaurantAnnotation(coordinate, title:title, subtitle:"")
annotation.image = globalImageArray[r]
map.addAnnotation(annotation)
}

Now, in your view for annotation you can access the image -

func mapView(mapView: MKMapView!, viewForAnnotation annotation: MKAnnotation!) -> MKAnnotationView! {
if !(annotation is RestaurantAnnotation) {
return nil
}

let reuseId = "restaurant"
var anView = mapView.dequeueReusableAnnotationViewWithIdentifier(reuseId)
if anView == nil {
anView = MKAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
anView.canShowCallout = true
}
else {
anView.annotation = annotation
}

let restaurantAnnotation = annotation as RestaurantAnnotation

if (restaurantAnnotation.image != nil) {
anView.image = restaurantAnnotation.image!
anView.image.layer.setCornerRadius(8.0)
anView.image.layer.clipsToBounds=true
}
else {
// Perhaps set some default image
}

return anView
}


Related Topics



Leave a reply



Submit