3

Based on this answer:

Snapshot of MKMapView

I tried to convert my map to picture, but the App never enters the snapshotter block.

Why?

//Get location an then get a Picture of the Map.
CLLocation *userLoc = self.map.userLocation.location; //self.map is a MKMapView;
CLLocationCoordinate2D punto = userLoc.coordinate;
MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(punto, 500, 500);
[self.map setRegion:(region)];
[self.map setShowsUserLocation:YES];
//Place a Pin in actual location.
MKPointAnnotation *pin = [[MKPointAnnotation alloc]init];
pin.coordinate = punto;
pin.title = @"Localización";
[self.map addAnnotation:pin];

//Convert map to picture.
MKMapSnapshotOptions *options = [[MKMapSnapshotOptions alloc] init];
options.region = self.map.region;
options.scale = [UIScreen mainScreen].scale;
options.size = self.map.frame.size;

MKMapSnapshotter *snapshotter = [[MKMapSnapshotter alloc] initWithOptions:options];
[snapshotter startWithQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) completionHandler:^(MKMapSnapshot *snapshot, NSError *error) {
    NSLog(@"Entering the block."); //Never prints!
    // get the image associated with the snapshot
    UIImage *image = snapshot.image;
    NSLog(@"imagen %@",image); //Niether do this!
    // 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.map.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();
    NSLog(@"Picture inside the block %@",finalImage);  //Never prints.
    UIGraphicsEndImageContext();
    // and save it
    NSData *data = UIImagePNGRepresentation(finalImage);
    [data writeToFile:@"Picture.jpg" atomically:YES];
    if (error) {
        NSLog(@"Error"); //This is not printed.
    }else{
        NSLog(@"Success!"); //Neither do this.
        self.fotoParaEnviar = finalImage;
    }
}];
NSLog(@"Picture outside the block %@",self.fotoParaEnviar); //This is allway NULL

Look like everything is instantiated fine.

So why does the block is never executed?

Community
  • 1
  • 1
  • I cannot reproduce this problem, but you're not the only person who has seen it: http://stackoverflow.com/questions/22774090/ios-mkmapshapshotter-completion-block-is-not-always-being-called – Rob May 09 '14 at 20:33

3 Answers3

1

If you are already displaying map, then there is no magic required to save it into image, Snapshot of MKMapView in iOS7 gets it almost correctly , I don't understand why they get black image, but I do not pass 0.0 as rendering scale, but 1.0 or 2.0 (retina) and maybe their code is not on the main thread as it should be for graphics.

Anyway, I've just tried this on 7.1 and got the correct image with user blue dot and annotation pins:

[ObCommons createJPEGfromView:self.map withSize:self.map.bounds.size toPath:[ObCommons getPathInDocuments:@"test.jpg"]];
+(UIImage*) createImageFromView:(UIView*)newt withSize:(CGSize)rensize {
    UIGraphicsBeginImageContextWithOptions(rensize, NO, 2.0);   // 1.0 or 2.0 for retina (get it from UIScreen)
    [newt.layer renderInContext:UIGraphicsGetCurrentContext()];
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();    
    UIGraphicsEndImageContext();

    return image;
}

+(UIImage*) createJPEGfromView:(UIView*)newt withSize:(CGSize)rensize toPath:  (NSString*)filePath quality:(float)quality{
    UIImage *ximage = [ObCommons createImageFromView:newt withSize:rensize];    
    NSData *imageData = UIImageJPEGRepresentation(ximage, quality);

    if (filePath!=nil) {
        [imageData writeToFile:filePath atomically:YES];
    }

    return ximage;
}

+(CGFloat)retinaFactor {
    if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)] && [[UIScreen mainScreen] scale] > 1) {
        return [[UIScreen mainScreen]scale];
    } else {
        return 1.0f;
    }
}

To be more readable, here is gist of associated methods: https://gist.github.com/quentar/d92e95728ce0d950db65

Community
  • 1
  • 1
PetrV
  • 1,368
  • 2
  • 15
  • 30
  • Yep, this is the tried and true technique for iOS versions prior to 7. In WWDC 2013 "Putting Map Kit in Perspective", though, they explicitly advise that we "throw away" our `renderInContext` code in conjunction with maps in iOS 7. Snapshotter offers all sorts of advantages (automatically called only after all the relevant tiles are retrieved, doesn't actually require map view at all, more robust error handling), but I guess if OP cannot get snapshotter to work, this might be a contingency plan. – Rob May 09 '14 at 21:14
  • +1 By the way, if you use the above technique, it might be prudent to put this inside `mapViewDidFinishRenderingMap:fullyRendered:` (for iOS 7, at least), so the screen snapshot won't be attempted until all of the data has been retrieved and the map has been fully rendered (or as much as it can). If you simply `renderInContext` before all of the map data has been retrieved, you can get a black image (or unnecessarily incomplete image). – Rob May 09 '14 at 21:26
0

What if you change this:

[snapshotter startWithQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)

To this:

[snapshotter startWithQueue:dispatch_get_main_queue()

And this NSLog(@"Picture inside the block %@",self.fotoParaEnviar); will be always NULL as snapshotter is async and by the time you reach your NSLog the code above still executs its block

Or you might also try this instad:

[snapshotter startWithCompletionHandler:^(MKMapSnapshot *snapshot, NSError *error) {
    UIImage *image = snapshot.image;
    // and so on
}];
Pancho
  • 4,099
  • 1
  • 21
  • 32
  • Panayot. I tried what you suggest: [snapshotter startWithQueue:dispatch_get_main_queue() I even tried: [snapshotter startWithCompletionHandler:^(MKMapSnapshot *snapshot, NSError *error) { But still not working. Why does this never prints: NSLog(@"Entering the block."); And if: NSLog(@"Picture inside the block %@",self.fotoParaEnviar); Is always null, at least it will print: Picture inside the block NULL But not, is not printed. Thanks for your time. – user3614102 May 09 '14 at 17:31
0

I will close this question with the following solution:

FIRST let the MKMapView load completly in the View, and then enter the block to convert it to a UIImage.

Thank you all for your help.

  • Could you please share with me how you did that in your code? I think I have a quite similar issue. Thanks. – SanitLee Feb 04 '15 at 18:22