Snapshot of MKMapView in iOS7
Asked Answered
E

3

44

I am trying to create a snapshot of a MKMapView in iOS7 application the same way it's recommended everywhere for previous iOS versions:

- (UIImage*) renderMapViewToImage
{
   UIGraphicsBeginImageContextWithOptions(mapView.frame.size, NO, 0.0);
   [mapView.layer renderInContext:UIGraphicsGetCurrentContext()];
   UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
   UIGraphicsEndImageContext(); 
   return image;
}

However, the image returned is a black rectangle with a blue current location dot on top of it. I've tried using different sublayers of the mapView as well, but the result is always the same.

Does anyone know how to take MKMapView snapshots in iOS7 ?

Eades answered 13/9, 2013 at 0:19 Comment(2)
why are you passing 0.0 as context options ? it should be 1.0 or 2.0 for retina displays ...Loran
@Qiqi Zero is fine. As the documentation for UIGraphicsBeginImageContextWithOptions says, "If you specify a value of 0.0, the scale factor is set to the scale factor of the device’s main screen."Darondarooge
D
119

You can use MKMapSnapshotter and grab the image from the resulting MKMapSnapshot. See the discussion of it WWDC 2013 session video, Putting Map Kit in Perspective.

For example:

MKMapSnapshotOptions *options = [[MKMapSnapshotOptions alloc] init];
options.region = self.mapView.region;
options.scale = [UIScreen mainScreen].scale;
options.size = self.mapView.frame.size;

MKMapSnapshotter *snapshotter = [[MKMapSnapshotter alloc] initWithOptions:options];
[snapshotter startWithCompletionHandler:^(MKMapSnapshot *snapshot, NSError *error) {
    UIImage *image = snapshot.image;
    NSData *data = UIImagePNGRepresentation(image);
    [data writeToFile:[self snapshotFilename] atomically:YES];
}];

Having said that, the renderInContext solution still works for me. There are notes about only doing that in the main queue in iOS7, but it still seems to work. But MKMapSnapshotter seems like the more appropriate solution for iOS7.


If you want to include some annotations in the snapshot, you have to draw them manually (!). This is discussed in some detail at the end of the Putting Map Kit in Perspective video. I have to say that this is one of the least elegant implementations that I've ever seen Apple advise. Anyway, in iOS, it might look like:

MKMapSnapshotOptions *options = [[MKMapSnapshotOptions alloc] init];
options.region = self.mapView.region;
options.scale = [UIScreen mainScreen].scale;
options.size = self.mapView.frame.size;

MKMapSnapshotter *snapshotter = [[MKMapSnapshotter alloc] initWithOptions:options];
[snapshotter startWithQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) completionHandler:^(MKMapSnapshot *snapshot, NSError *error) {

    // get the image associated with the snapshot

    UIImage *image = snapshot.image;

    // Get the size of the final image

    CGRect finalImageRect = CGRectMake(0, 0, image.size.width, image.size.height);

    // Get a standard annotation view pin. Clearly, Apple assumes that we'll only want to draw standard annotation pins!

    MKAnnotationView *pin = [[MKPinAnnotationView alloc] initWithAnnotation:nil reuseIdentifier:@""];
    UIImage *pinImage = pin.image;

    // ok, let's start to create our final image

    UIGraphicsBeginImageContextWithOptions(image.size, YES, image.scale);

    // first, draw the image from the snapshotter

    [image drawAtPoint:CGPointMake(0, 0)];

    // now, let's iterate through the annotations and draw them, too

    for (id<MKAnnotation>annotation in self.mapView.annotations)
    {
        CGPoint point = [snapshot pointForCoordinate:annotation.coordinate];
        if (CGRectContainsPoint(finalImageRect, point)) // this is too conservative, but you get the idea
        {
            CGPoint pinCenterOffset = pin.centerOffset;
            point.x -= pin.bounds.size.width / 2.0;
            point.y -= pin.bounds.size.height / 2.0;
            point.x += pinCenterOffset.x;
            point.y += pinCenterOffset.y;

            [pinImage drawAtPoint:point];
        }
    }

    // grab the final image

    UIImage *finalImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    // and save it

    NSData *data = UIImagePNGRepresentation(finalImage);
    [data writeToFile:[self snapshotFilename] atomically:YES];
}];

For MacOS implementation, see that video for more information, but the technique is basically the same (the mechanism for creating the images is slightly different).

Darondarooge answered 13/9, 2013 at 1:15 Comment(12)
Thanks for the excellent answer! It works now. renderInContext still doesn't work for me, but that's another story.Eades
Is there any way to render a pin annotation with MKMapSnapshotter?Mineralogist
@Mineralogist At the end of that video I reference in my answer, Apple walks you precisely through this technique. It's ugly though. I'm surprised that they think this is an acceptable implementation. Anyway, I've included a rendition here and updated my answer accordingly. You'll need to modify it if not using pin annotations, if you have MKUserAnnotation, etc., but this should point you in the right direction.Darondarooge
Great answer! I didn't go till the end of Apple's video, and I can't agree more: this isn't elegant at all. The whole idea of this cool new API is not to rely on Image Context rendering and using MapKit additional APIs. But it works and looks like a charm. Here's a screeshot: dl.dropboxusercontent.com/u/2452151/Permalink/…Mineralogist
Thanks, it worked well. Is there anyone tried with custom annotation image?Witenagemot
@TienDo I haven't, but it shouldn't be any worse than the above.Darondarooge
@Darondarooge Yeah, just one more line to add pin image file, and for anyone who haven't tried it, add @ 2x to file name for retina screen. Thanks again :)Witenagemot
I did work on using custom annotation views. Basically I "duplicated" the drawRect and have it return an image instead. Needs a little bit of adjustment regarding the coordinates, but it's not too much work.Cartagena
Can this be done without having an instance of a MapView?Lepine
@jeraldo That is correct, no map view is needed. Clearly, you'd have to set the camera or region of the MKMapSnapshotOptions some other way (the above just uses the region of the map view), but yes, MKMapSnapshotter works perfectly fine without a map view. In fact, it's often used for precisely that reason, when you don't want the overhead of the map view and a static rendition is acceptable.Darondarooge
@Darondarooge since you use startWithQueue: with a background queue, it is not safe to use: MKAnnotationView *pin = [[MKPinAnnotationView alloc] initWithAnnotation:nil reuseIdentifier:@""]; in the callback.Kuhlman
@Kuhlman I do not believe that is true. I took this directly from Apple's demo. There actually are limited view rendering options available from background queues and I believe this is one of them.Darondarooge
I
8

For iOS 10 and above you can use UIGraphicsImageRenderer class for rendering any view to image (just in case if you don't want to use MKMapSnapshotter, since i am using MapBox).

let render = UIGraphicsImageRenderer(size: self.mapView.bounds.size)
let image = render.image { ctx in
  self.mapView.drawHierarchy(in: self.mapView.bounds, afterScreenUpdates: true)
}

Result:

enter image description here

Indention answered 14/7, 2018 at 13:46 Comment(0)
S
7

For Swift 3

Here is a swift 3 version I modified from this article: Render a Map as an Image using MapKit

The following code allows you to snapshot a region based on both Point(1 coordinate) and Polyline(several coordinates)

func takeSnapShot() {
    let mapSnapshotOptions = MKMapSnapshotOptions()

    // Set the region of the map that is rendered. (by one specified coordinate)
    // let location = CLLocationCoordinate2DMake(24.78423, 121.01836) // Apple HQ
    // let region = MKCoordinateRegionMakeWithDistance(location, 1000, 1000)

    // Set the region of the map that is rendered. (by polyline)
    // var yourCoordinates = [CLLocationCoordinate2D]()  <- initinal this array with your polyline coordinates
    let polyLine = MKPolyline(coordinates: &yourCoordinates, count: yourCoordinates.count)
    let region = MKCoordinateRegionForMapRect(polyLine.boundingMapRect)

    mapSnapshotOptions.region = region

    // Set the scale of the image. We'll just use the scale of the current device, which is 2x scale on Retina screens.
    mapSnapshotOptions.scale = UIScreen.main.scale

    // Set the size of the image output.
    mapSnapshotOptions.size = CGSize(width: IMAGE_VIEW_WIDTH, height: IMAGE_VIEW_HEIGHT)

    // Show buildings and Points of Interest on the snapshot
    mapSnapshotOptions.showsBuildings = true
    mapSnapshotOptions.showsPointsOfInterest = true

    let snapShotter = MKMapSnapshotter(options: mapSnapshotOptions)

    snapShotter.start() { snapshot, error in
        guard let snapshot = snapshot else {
            return
        }
        self.imageView.image = snapshot.image
    }
}
Singly answered 8/2, 2017 at 7:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.