iOS: what's the fastest, most performant way to make a screenshot programmatically?
Asked Answered
H

6

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.

Hypotenuse answered 1/11, 2011 at 8:42 Comment(1)
Don't forget to call UIGraphicsEndImageContext when you're done.Ballard
A
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;
}
Albina answered 5/11, 2011 at 0:41 Comment(13)
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.Reek
@GregMaletic: Yes, the other solution it looks simpler but it works over UIView, this one works over UIWindow so it's more complete.Albina
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?Batt
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.Sall
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];Kienan
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: developer.apple.com/library/ios/qa/qa1714/_index.htmlPressure
I should add this does not appear to work for iOS 6 and below. It does now work for iOS 7.Pressure
+1 for having the only answer on SA I found that supports all UIInterfaceOrientationsBuckden
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.Concertmaster
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?Phono
@DmitryFink if it is, I haven't seen it documented anywhereAlbina
This work fine but it's doesn't take screenshot for the camera session or view #41239754Nonnah
In my tests, renderInContext also performs much better than drawViewHierarchyInRect, iOS 11.2Potsdam
T
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

Threepiece answered 1/11, 2011 at 10:25 Comment(5)
thanks a lot; I'm using a category right now; but I'm looking for a more performant way to make the screenshot... :/Hypotenuse
@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...Hypotenuse
Same for me... isn't there a way without re-rendering everything?Solvent
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.Threepiece
I needed to use afterScreenUpdates:YES, but otherwise, it works great.Waken
D
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;
    }
}
Derisible answered 20/9, 2013 at 20:44 Comment(3)
Have you tested that it is actually faster? My tests resulted in very little performance improvements, even with afterScreenUpdates set to NO.Darky
@Darky 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.Ardithardme
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()) .Obumbrate
Y
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;
}
Yaelyager answered 9/1, 2014 at 6:14 Comment(0)
D
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.

Darky answered 11/10, 2013 at 21:26 Comment(4)
Can you tell me exactly what kind of differences you are seeing? I am seeing a slight increase in time.Dihydric
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 viewsDarky
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.Dihydric
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. #11435710Darky
C
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.

Consecration answered 7/11, 2011 at 23:34 Comment(2)
so there's no quicker solution?Hypotenuse
it isn't on ios, since gpu and cpu share the same ramExtrovert

© 2022 - 2024 — McMap. All rights reserved.