Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to render a CALayer in the background

I need to save screenshots from my app, so I've set up code like this, which works:

- (void)renderScreen {
    UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];

    CGSize outputSize = keyWindow.bounds.size;
    UIGraphicsBeginImageContext(outputSize);
    CGContextRef context = UIGraphicsGetCurrentContext();

    CGContextSaveGState(context);
    CALayer *layer = [keyWindow layer];
    [layer renderInContext:context];
    CGContextRestoreGState(context);

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

    // now save the screen image, etc...
}

However, when the screen image becomes complex (lots of views), the renderInContext can take up to 0.8 seconds on an iPad 3, and the user interface locks up during that time, which interferes with some other functionality. So I moved the rendering to a background thread, like this:

- (void)renderScreen {
    UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];
    CALayer *layer = [keyWindow layer];
    [self performSelectorInBackground:@selector(renderLayer:) withObject:layer];
}

- (void)renderLayer:(CALayer *)layer {
    UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];

    CGSize outputSize = keyWindow.bounds.size;
    UIGraphicsBeginImageContext(outputSize);
    CGContextRef context = UIGraphicsGetCurrentContext();

    CGContextSaveGState(context);
    [layer renderInContext:context];
    CGContextRestoreGState(context);

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

    // now save the screen image, etc...
}

That allows the interface to run smoothly again, but occasionally causes a crash with EXC_BAD_ACCESS on the renderInContext line. I tried checking for layer!=nil and [layer respondsToSelector:@selector(renderInContext:)] first, so I could avoid the crash, but both conditions always return true.

Then I read this SO comment, stating that a layer could mutate before the background operation runs and suggesting sending a copy of the layer to the background operation instead. This SO answer and this one got me started, and I ended up with this category to add a copy method to CALayer:

#import "QuartzCore/CALayer.h"

@interface CALayer (CALayerCopyable)
- (id)copy;
@end

@implementation CALayer (CALayerCopyable)

- (id)copy {
    CALayer *newLayer = [CALayer layer];
    newLayer.actions = [self.actions copy];
    newLayer.anchorPoint = self.anchorPoint;
    newLayer.anchorPointZ = self.anchorPointZ;
    newLayer.backgroundColor = self.backgroundColor;
    //newLayer.backgroundFilters = [self.backgroundFilters copy]; // iOS 5+
    newLayer.borderColor = self.borderColor;
    newLayer.borderWidth = self.borderWidth;
    newLayer.bounds = self.bounds;
    //newLayer.compositingFilter = self.compositingFilter; // iOS 5+
    newLayer.contents = [self.contents copy];
    newLayer.contentsCenter = self.contentsCenter;
    newLayer.contentsGravity = [self.contentsGravity copy];
    newLayer.contentsRect = self.contentsRect;
    //newLayer.contentsScale = self.contentsScale; // iOS 4+
    newLayer.cornerRadius = self.cornerRadius;
    newLayer.delegate = self.delegate;
    newLayer.doubleSided = self.doubleSided;
    newLayer.edgeAntialiasingMask = self.edgeAntialiasingMask;
    //newLayer.filters = [self.filters copy]; // iOS 5+
    newLayer.frame = self.frame;
    newLayer.geometryFlipped = self.geometryFlipped;
    newLayer.hidden = self.hidden;
    newLayer.magnificationFilter = [self.magnificationFilter copy];
    newLayer.mask = [self.mask copy]; // property is another CALayer
    newLayer.masksToBounds = self.masksToBounds;
    newLayer.minificationFilter = [self.minificationFilter copy];
    newLayer.minificationFilterBias = self.minificationFilterBias;
    newLayer.name = [self.name copy];
    newLayer.needsDisplayOnBoundsChange = self.needsDisplayOnBoundsChange;
    newLayer.opacity = self.opacity;
    newLayer.opaque = self.opaque;
    newLayer.position = self.position;
    newLayer.rasterizationScale = self.rasterizationScale;
    newLayer.shadowColor = self.shadowColor;
    newLayer.shadowOffset = self.shadowOffset;
    newLayer.shadowOpacity = self.shadowOpacity;
    newLayer.shadowPath = self.shadowPath;
    newLayer.shadowRadius = self.shadowRadius;
    newLayer.shouldRasterize = self.shouldRasterize;
    newLayer.style = [self.style copy];
    //newLayer.sublayers = [self.sublayers copy]; // this line makes the screen go blank
    newLayer.sublayerTransform = self.sublayerTransform;
    //newLayer.superlayer = self.superlayer; // read-only
    newLayer.transform = self.transform;
    //newLayer.visibleRect = self.visibleRect; // read-only
    newLayer.zPosition = self.zPosition;
    return newLayer;
}

@end

Then I updated renderScreen to send a copy of the layer to renderLayer:

- (void)renderScreen {
    UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];
    CALayer *layer = [keyWindow layer];
    CALayer *layerCopy = [layer copy];
    [self performSelectorInBackground:@selector(renderLayer:) withObject:layerCopy];
}

When I run this code, all the screen images are plain white. Obviously my copy method is not correct. So can someone help me with any of the following possible solutions?

  1. How to write a copy method for CALayer that really works?
  2. How to check that a layer passed into a background process is a valid target for renderInContext?
  3. Any other way to render complex layers without locking up the interface?

UPDATE: I rewrote my CALayerCopyable category based on Rob Napier's suggestion to use initWithLayer. Simply copying the layer still gave me a plain white output, so I added a method to recursively copy all the sublayers. I still, however, get the plain white output:

#import "QuartzCore/CALayer.h"

@interface CALayer (CALayerCopyable)
- (id)copy;
- (NSArray *)copySublayers:(NSArray *)sublayers;
@end

@implementation CALayer (CALayerCopyable)

- (id)copy {
    CALayer *newLayer = [[CALayer alloc] initWithLayer:self];
    newLayer.sublayers = [self copySublayers:self.sublayers];
    return newLayer;
}

- (NSArray *)copySublayers:(NSArray *)sublayers {
    NSMutableArray *newSublayers = [NSMutableArray arrayWithCapacity:[sublayers count]];
    for (CALayer *sublayer in sublayers) {
        [newSublayers addObject:[sublayer copy]];
    }
    return [NSArray arrayWithArray:newSublayers];
}

@end
like image 317
arlomedia Avatar asked Aug 29 '12 20:08

arlomedia


1 Answers

For this purpose, I'd use initWithLayer: rather than creating your own copy method. initWithLayer: is explicitly for creating "shadow copies of layers, for example, for the presentationLayer method."

You may also need to create copies of the sublayers. I can't remember immediately whether initWithLayer: does that for you. But initWithLayer: is how Core Animation works, so it's optimized for problems like this.

like image 132
Rob Napier Avatar answered Nov 15 '22 04:11

Rob Napier