Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 512
swalkner Avatar asked Nov 01 '11 08:11

swalkner


6 Answers

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;
}
like image 173
3lvis Avatar answered Nov 14 '22 21:11

3lvis



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

like image 38
Trenskow Avatar answered Nov 14 '22 20:11

Trenskow


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;
    }
}
like image 10
Klaas Avatar answered Nov 14 '22 21:11

Klaas


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;
}
like image 3
Hemang Avatar answered Nov 14 '22 22:11

Hemang


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.

like image 2
maxpower Avatar answered Nov 14 '22 22:11

maxpower


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.

like image 1
David Dunham Avatar answered Nov 14 '22 21:11

David Dunham