31

In my app, I use drawViewHierarchyInRect:afterScreenUpdates: in order to obtain a blurred image of my view (using Apple’s UIImage category UIImageEffects).

My code looks like this:

UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, 0);
[self.view drawViewHierarchyInRect:self.view.bounds afterScreenUpdates:YES];
UIImage *im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
/* Use im */

I noticed during development that many of my animations were delayed after using my app for a bit, i.e., my views were beginning their animations after a noticeable (but less than about a second) pause compared to a fresh launch of the app.

After some debugging, I noticed that the mere act of using drawViewHierarchyInRect:afterScreenUpdates: with screen updates set to YES caused this delay. If this message was never sent during a session of usage, the delay never appeared. Using NO for the screen updates parameter also made the delay disappear.

The strange thing is that this blurring code is completely unrelated (as far as I can tell) to the delayed animations. The animations in question do not use drawViewHierarchyInRect:afterScreenUpdates:, they are CAKeyframeAnimation animations. The mere act of sending this message (with screen updates set to YES) seems to have globally affected animations in my app.

What’s going on?

(I have created videos illustrating the effect: with and without an animation delay. Note the delay in the appearance of the "Check!" speech bubble in the navigation bar.)

UPDATE

I have created an example project to illustrate this potential bug. https://github.com/timarnold/AnimationBugExample

UPDATE No. 2

I received a response from Apple verifying that this is a bug. See answer below.

Community
  • 1
  • 1
Tim Arnold
  • 8,359
  • 8
  • 44
  • 67
  • I ran into a similar issue where blurring would cause a heavy lag in UI updates. We solved it by making sure that animations were done on the main thread.. does that help? -Useful vids btw. But not sure what exactly to look for though. – joels Apr 20 '14 at 04:24
  • look at how long it takes for the "Check!" speech bubble to appear after the game view controller appears – Tim Arnold Jun 07 '14 at 01:26
  • If you use afterScreenUpdates:NO, it doesn't lag. – Borzh May 01 '19 at 20:01
  • follow this. https://stackoverflow.com/a/76670609/13671576 – sagarthecoder Jul 12 '23 at 12:44

5 Answers5

78

I used one of my Apple developer support tickets to ask Apple about my issue.

It turns out it is a confirmed bug (radar number 17851775). Their hypothesis for what is happening is below:

The method drawViewHierarchyInRect:afterScreenUpdates: performs its operations on the GPU as much as possible, and much of this work will probably happen outside of your app’s address space in another process. Passing YES as the afterScreenUpdates: parameter to drawViewHierarchyInRect:afterScreenUpdates: will cause a Core Animation to flush all of its buffers in your task and in the rendering task. As you may imagine, there’s a lot of other internal stuff that goes on in these cases too. Engineering theorizes that it may very well be a bug in this machinery related to the effect you are seeing.

In comparison, the method renderInContext: performs its operations inside of your app’s address space and does not use the GPU based process for performing the work. For the most part, this is a different code path and if it is working for you, then that is a suitable workaround. This route is not as efficient as it does not use the GPU based task. Also, it is not as accurate for screen captures as it may exclude blurs and other Core Animation features that are managed by the GPU task.

And they also provided a workaround. They suggested that instead of:

UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, 0);
[self.view drawViewHierarchyInRect:self.view.bounds afterScreenUpdates:YES];
UIImage *im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
/* Use im */

I should do this

UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, 0);
[self.view.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
/* Use im */

Hopefully this is helpful for someone!

Community
  • 1
  • 1
Tim Arnold
  • 8,359
  • 8
  • 44
  • 67
  • 3
    I've actually noticed a different issue that's caused and solved by the same calls/solution. In my case, the draw calls seems to temporarily draw the view hierarchy to screen with an incorrect scale on an iPhone 6 & 6 Plus. I'm guessing it draws the screen at it's native resolution before it's scaled down, but it displays this on screen. The issue is once again solved with the code above. – TheCodingArt May 16 '15 at 02:58
  • @TheCodingArt I've seen this too – Tim Arnold Aug 13 '15 at 14:02
  • Do you see the same defect if you pass `UIScreen.scale` rather than 0.0 as the last parameter to `UIGraphicsBeginImageContextWithOptions` ? – verec Nov 04 '15 at 06:41
  • @verec I don't know, that's a good question. I haven't seen the bug in a very long time and so I'm not sure it'd be easy to reproduce (or if it is still an issue). Are you still seeing the issue? – Tim Arnold Nov 09 '15 at 14:02
  • @TimCamber no I haven't since I have always used `UIScreen.mainScreen().scale` rather than 0.0. My hunch is that since Apple added `nativeScale` to `UIScreen` to help tell 2x vs 3x, the 0.0 expands to 3.0 on 6x, but to 2.0 if you force it to the "logical scale" as returned by `scale` rather than `nativeScale`. I'll dig into this some day :-) – verec Nov 11 '15 at 14:16
  • Using "renderInContext" has some problem when we take screenshot with segment controllers. Selected segment controller text will hide in the screenshot. See this http://stackoverflow.com/questions/21388335/title-for-selected-segment-in-uisegmentedcontrol-disappears-when-taking-a-screen – Confused Feb 18 '16 at 23:38
  • I was using the `renderInContext` method, but it turns out that it causes a memory leak. So I changed to `drawViewHierarchyInRect`, and now it blocks and destroys the animations I had in place. I think I will have to live with one bug or the other.. Anyone knows about the memory issue? I can't find good answers. – Manuel M. Oct 03 '16 at 15:27
  • 1
    This work around is nice if you don't have a 3D transform applied to the view's layer. Unfortunately, renderInContext doesn't support 3D transforms. – VTPete Dec 09 '18 at 14:15
  • This answer is very wrong and obsolete. `renderInContext` indeed does on-CPU rendering in-process and thus doesn't support full CoreAnimation blending model, so produced images will not look the same as on screen. It also slow. You need to use `-[UIView snapshotViewAfterScreenUpdates:]` (preferably specifying `NO`) as much as possible, since it's most performant and memory efficient. If you really want a persistent image (which I doubt), then use `-[UIView drawViewHierarchyInRect:]`. – wonder.mice Jan 04 '19 at 19:02
4

I tried all the latest snapshot methods using swift. Other methods didn't work for me in the background. But taking snapshot this way worked for me.

create an extension with parameters view layer and view bounds.

extension UIView {
    func asImage(viewLayer: CALayer, viewBounds: CGRect) -> UIImage {
        if #available(iOS 10.0, *) {
            let renderer = UIGraphicsImageRenderer(bounds: viewBounds)
            return renderer.image { rendererContext in
                viewLayer.render(in: rendererContext.cgContext)
            }
        } else {
            UIGraphicsBeginImageContext(viewBounds.size)
            viewLayer.render(in:UIGraphicsGetCurrentContext()!)
            let image = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()
            return UIImage(cgImage: image!.cgImage!)
        }
    }
}

Usage

DispatchQueue.main.async {
                let layer = self.selectedView.layer
                let bounds = self.selectedView.bounds
                DispatchQueue.global(qos: .background).async {
                    let image = self.selectedView.asImage(viewLayer: layer, viewBounds: bounds)
                }
            }

We need to calculate layer and bounds in the main thread, then other operations will work in the background thread. It will give smooth user experience without any lag or interruption in UI.

Faris Muhammed
  • 970
  • 13
  • 17
2

Why do you have this line (from your sample app):

animation.beginTime = CACurrentMediaTime();

Just remove it, and everything will be as you want it to be.

By setting animation time explicitly to CACurrentMediaTime() you ignore possible time transformations that can be present in layer tree. Either don't set it at all (by default animations will start now) or use time conversion method:

animation.beginTime = [view.layer convert​Time:CACurrentMediaTime() from​Layer:nil];

UIKit adds time transformations to layer tree when you call afterScreenUpdates:YES to prevent jumps in ongoing animation, that would be caused otherwise by intermediate CoreAnimation commits. If you want to start animation at specific time (not now), use time conversion method mentioned above.

And while at it, strongly prefer using -[UIView snapshotViewAfterScreenUpdates:] and friends instead of -[UIView drawViewHierarchyInRect:] family (preferably specifying NO for afterScreenUpdates part). In most of the cases you don't really need a persistent image and view snapshot is what you actually want. Using view snapshot instead of rendered image has following benefits:

  • 2x-10x faster
  • Uses 2x-3x less memory
  • It will always use correct colorspace and buffer format (e.g. on devices with wide color screen)
  • It will use correct scale and orientation, so you don't need to think how to position your image so it looks good.
  • It works with accessibility features better (e.g. with Smart Invert colors)
  • View snapshot will also capture out-of-process and secure views correctly (while drawViewHierarchyInRect will render them black or white).
wonder.mice
  • 7,227
  • 3
  • 36
  • 39
1

When the afterScreenUpdates parameter is set to YES, the system has to wait until all pending screen updates have happened before it can render the view.

If you're kicking off animations at the same time then perhaps the rendering and the animations are trying to happen together and this is causing a delay.

It may be worth experimenting with kicking off your animations slightly later to prevent this. Obviously not too much later because that would defeat the object, but a small dispatch_after interval would be worth trying.

jrturton
  • 118,105
  • 32
  • 252
  • 268
  • That would make sense, except the animations are happening at a *very different time* than the screenshotting. – Tim Arnold Jun 07 '14 at 01:26
0

Have you tried running your code on a background thread? Heres an example using gcd:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
    //background thread
    UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, 0);
    [self.view drawViewHierarchyInRect:self.view.bounds afterScreenUpdates:YES];
    UIImage *im = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    dispatch_sync(dispatch_get_main_queue(), ^(void) {
        //update ui on main thread
    });
});
Sam
  • 864
  • 7
  • 15