79

in my iPad app, I'd like to make a screenshot of a UIView taking a big part of the screen. Unfortunately, the subviews are pretty deeply nested, so it takes to long to make the screenshot and animate a page curling afterwards.

Is there a faster way than the "usual" one?

UIGraphicsBeginImageContext(self.bounds.size);
[self.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *resultingImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

If possible, I'd like to avoid caching or restructuring my view.

Cœur
  • 37,241
  • 25
  • 195
  • 267
swalkner
  • 16,679
  • 31
  • 123
  • 210

6 Answers6

113

I've found a better method that uses the snapshot API whenever possible.

I hope it helps.

class func screenshot() -> UIImage {
    var imageSize = CGSize.zero

    let orientation = UIApplication.shared.statusBarOrientation
    if UIInterfaceOrientationIsPortrait(orientation) {
        imageSize = UIScreen.main.bounds.size
    } else {
        imageSize = CGSize(width: UIScreen.main.bounds.size.height, height: UIScreen.main.bounds.size.width)
    }

    UIGraphicsBeginImageContextWithOptions(imageSize, false, 0)
    for window in UIApplication.shared.windows {
        window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
    }

    let image = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return image!
}

Wanna know more about iOS 7 Snapshots?

Objective-C version:

+ (UIImage *)screenshot
{
    CGSize imageSize = CGSizeZero;

    UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation;
    if (UIInterfaceOrientationIsPortrait(orientation)) {
        imageSize = [UIScreen mainScreen].bounds.size;
    } else {
        imageSize = CGSizeMake([UIScreen mainScreen].bounds.size.height, [UIScreen mainScreen].bounds.size.width);
    }

    UIGraphicsBeginImageContextWithOptions(imageSize, NO, 0);
    CGContextRef context = UIGraphicsGetCurrentContext();
    for (UIWindow *window in [[UIApplication sharedApplication] windows]) {
        CGContextSaveGState(context);
        CGContextTranslateCTM(context, window.center.x, window.center.y);
        CGContextConcatCTM(context, window.transform);
        CGContextTranslateCTM(context, -window.bounds.size.width * window.layer.anchorPoint.x, -window.bounds.size.height * window.layer.anchorPoint.y);
        if (orientation == UIInterfaceOrientationLandscapeLeft) {
            CGContextRotateCTM(context, M_PI_2);
            CGContextTranslateCTM(context, 0, -imageSize.width);
        } else if (orientation == UIInterfaceOrientationLandscapeRight) {
            CGContextRotateCTM(context, -M_PI_2);
            CGContextTranslateCTM(context, -imageSize.height, 0);
        } else if (orientation == UIInterfaceOrientationPortraitUpsideDown) {
            CGContextRotateCTM(context, M_PI);
            CGContextTranslateCTM(context, -imageSize.width, -imageSize.height);
        }
        if ([window respondsToSelector:@selector(drawViewHierarchyInRect:afterScreenUpdates:)]) {
            [window drawViewHierarchyInRect:window.bounds afterScreenUpdates:YES];
        } else {
            [window.layer renderInContext:context];
        }
        CGContextRestoreGState(context);
    }

    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}
3lvis
  • 4,190
  • 1
  • 30
  • 35
  • 2
    Does this solution perform any better than the solution offered by the original poster? My own tests suggest it's exactly the same. Overall, I'd go with the original solution since the code is so much simpler. – Greg Maletic May 08 '12 at 23:44
  • @GregMaletic: Yes, the other solution it looks simpler but it works over UIView, this one works over UIWindow so it's more complete. – 3lvis May 09 '12 at 23:07
  • I still can't understand why this solution is faster. Most iOS apps contains just one window. Isn't just [self.window.layer renderInContext:context] should be fine? – Muhammad Hassan Nasr Nov 08 '12 at 23:23
  • 1
    I don't believe this works. The performance problems of renderInContext are well documented, and calling it on the Window's layer is not going to fix that. – Adam Feb 22 '13 at 21:11
  • This is an **almost** perfect answer ! One tiny flaw though, in case of layer transformations in your UIView, you might consider rendering the `presentationLayer` instead of the layer itself, likeso :`[window.layer.presentationLayer renderInContext:context];` – Alexis C. Oct 19 '13 at 22:34
  • This does not appear to work if one of your views uses AVCaptureVideoPreviewLayer (or other possible camera elements). I think the following Apple Tech Q&A helps with this but I have not confirmed: https://developer.apple.com/library/ios/qa/qa1714/_index.html – kanso Jan 02 '14 at 15:31
  • I should add this does not appear to work for iOS 6 and below. It does now work for iOS 7. – kanso Jan 02 '14 at 15:55
  • +1 for having the only answer on SA I found that supports all UIInterfaceOrientations – Jasper Mar 05 '14 at 11:10
  • Does not work in iOS8. To make it work you need to ` imageSize = [UIScreen mainScreen].bounds.size;` all the time and to skip the flipping part. – Antzi Feb 04 '15 at 04:53
  • I am trying it on IOS9, and I consistently getting renderInContext outperforming drawViewHierarchyInRect. By a lot. drawViewHierarchyInRect takes 0.15s to render, while renderInContext does 0.02 and below. Is it a new feature of IOS9? – Dmitry Fink Nov 06 '15 at 06:36
  • @DmitryFink if it is, I haven't seen it documented anywhere – 3lvis Nov 06 '15 at 07:20
  • This work fine but it's doesn't take screenshot for the camera session or view http://stackoverflow.com/questions/41239254/take-screenshot-from-camera-view – Chlebta Dec 21 '16 at 14:00
  • 1
    In my tests, renderInContext also performs much better than drawViewHierarchyInRect, iOS 11.2 – Nikolay Dimitrov Jan 16 '18 at 00:40
19

EDIT October 3. 2013 Updated to support the new super fast drawViewHierarchyInRect:afterScreenUpdates: method in iOS 7.


No. CALayer's renderInContext: is as far as I know the only way to do this. You could create a UIView category like this, to make it easier for yourself going forward:

UIView+Screenshot.h

#import <UIKit/UIKit.h>

@interface UIView (Screenshot)

- (UIImage*)imageRepresentation;

@end

UIView+Screenshot.m

#import <QuartzCore/QuartzCore.h>
#import "UIView+Screenshot.h"

@implementation UIView (Screenshot)

- (UIImage*)imageRepresentation {

    UIGraphicsBeginImageContextWithOptions(self.bounds.size, YES, self.window.screen.scale);

    /* iOS 7 */
    if ([self respondsToSelector:@selector(drawViewHierarchyInRect:afterScreenUpdates:)])            
        [self drawViewHierarchyInRect:self.bounds afterScreenUpdates:NO];
    else /* iOS 6 */
        [self.layer renderInContext:UIGraphicsGetCurrentContext()];

    UIImage* ret = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();

    return ret;

}

@end

By this you might be able to say [self.view.window imageRepresentation] in a view controller, and get a full screenshot of your app. This might exclude the statusbar though.

EDIT:

And may I add. If you have an UIView with transparent content, and needs an image representation WITH the underlaying content as well, you can grab an image representation of the container view and crop that image, simply by taking the rect of the subview and converting it to the container views coordinate system.

[view convertRect:self.bounds toView:containerView]

To crop see answer to this question: Cropping an UIImage

Community
  • 1
  • 1
Trenskow
  • 3,783
  • 1
  • 29
  • 35
  • thanks a lot; I'm using a category right now; but I'm looking for a more performant way to make the screenshot... :/ – swalkner Nov 01 '11 at 10:29
  • @EDIT: that's what I'm doing - I get the image representation of the container. But that's not helping me with my problem regarding performance... – swalkner Nov 01 '11 at 11:32
  • Same for me... isn't there a way without re-rendering everything? – Christian Schnorr Nov 01 '11 at 12:39
  • It is true, that iOS uses internal image representations in order to speed up rendering. Only views that change is rerendered. But if you're asking how to get the internal image representation, without the need to redraw, I do not think that is possible. As mentioned above, this image probably lives in the GPU, and is most probably unaccessible through public API's. – Trenskow Nov 11 '11 at 15:21
  • I needed to use `afterScreenUpdates:YES`, but otherwise, it works great. – EthanB Jan 02 '14 at 19:53
10

iOS 7 introduced a new method that allows you to draw a view hierarchy into the current graphics context. This can be used to get an UIImage very fast.

Implemented as category method on UIView:

- (UIImage *)pb_takeSnapshot {
    UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, [UIScreen mainScreen].scale);

    [self drawViewHierarchyInRect:self.bounds afterScreenUpdates:YES];

    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}

It is considerably faster then the existing renderInContext: method.

UPDATE FOR SWIFT: An extension that does the same:

extension UIView {

    func pb_takeSnapshot() -> UIImage {
        UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, UIScreen.mainScreen().scale);

        self.drawViewHierarchyInRect(self.bounds, afterScreenUpdates: true)

        // old style: self.layer.renderInContext(UIGraphicsGetCurrentContext())

        let image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        return image;
    }
}
Klaas
  • 22,394
  • 11
  • 96
  • 107
  • Have you tested that it is actually faster? My tests resulted in very little performance improvements, even with afterScreenUpdates set to NO. – maxpower Oct 11 '13 at 21:27
  • @maxpower I timed the execution and I got a more than 50% speed increase. With the old renderInContext: it took about 0.18s and with this it took 0.063. I believe your results will vary depending on the CPU in your device. –  Dec 16 '14 at 15:27
  • is it just me or does `self.drawViewHierarchyInRect(self.bounds, afterScreenUpdates: true)` cause an odd display bug for a moment while it executes? I don't get the same issue with `self.layer.renderInContext(UIGraphicsGetCurrentContext())` . – CaptainCOOLGUY Dec 22 '14 at 17:12
3

I combined the answers to single function which will be running for any iOS versions, even for retina or non-retains devices.

- (UIImage *)screenShot {
    if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)])
        UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, [UIScreen mainScreen].scale);
    else
        UIGraphicsBeginImageContext(self.view.bounds.size);

    #ifdef __IPHONE_7_0
        #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000
            [self.view drawViewHierarchyInRect:self.view.bounds afterScreenUpdates:YES];
        #endif
    #else
            [self.view.layer renderInContext:UIGraphicsGetCurrentContext()];
    #endif

    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}
Hemang
  • 26,840
  • 19
  • 119
  • 186
2

For me setting the InterpolationQuality went a long way.

CGContextSetInterpolationQuality(ctx, kCGInterpolationNone);

If you are snapshotting very detailed images this solution may not be acceptable. If you are snapshotting text you will hardly notice the difference.

This cut down the time to take the snap shot significantly as well as making an image that consumed far less memory.

This is still beneficial with the drawViewHierarchyInRect:afterScreenUpdates: method.

maxpower
  • 1,203
  • 11
  • 20
  • Can you tell me exactly what kind of differences you are seeing? I am seeing a slight increase in time. – daveMac Feb 07 '14 at 21:12
  • Unfortunately I can not. I no longer have access to the project. Changed jobs. but I can say the view that was being screen shotted had probably 50 +- 10 views in it descending hierarchy. I can also say that about 1/4 - 1/3 of the views were image views – maxpower Feb 13 '14 at 04:49
  • 1
    In researching things further, the only time I see any difference at all setting the interpolation, is if you are resizing the view when rendering it out or rendering it into a smaller context. – daveMac Feb 13 '14 at 15:43
  • I assume it mostly depends on the particular context in question. At least one other person has seen significant results from this. see the comment on this answer. http://stackoverflow.com/questions/11435210/ipad-3-renderincontext-slow-bad-rendering-performance/16453462#16453462 – maxpower Feb 14 '14 at 16:11
1

What you’re asking for as an alternative is to read the GPU (since the screen is composited from any number of translucent views), which is an inherently slow operation too.

David Dunham
  • 8,139
  • 3
  • 28
  • 41