Search code examples
cocoacore-animation

CATransaction begin/commit in loop not being honored, all happens at once


I'm working through the 4th ed. of the Hillegass/Preble Cocoa book and have hit a snag I can't understand in Core Animation.

In particular, I have the following loop that presents a list of images, supposedly one-at-a-time, into a layer-hosting view. The expected result is that I should see each image fly out one-at-a-time as presentImage is called. The actual result is that the view stays empty until the loop is complete and then the images fly out all-at-once. During the delay, I see the multiple log messages indicating the multiple calls to presentImage. When those log messages stop, indicating the loop is done, then all the images fly out at once.

// loop to present each image in list of URLs
// called at end of applicationDidFinishLaunching
NSTimeInterval t0 = [NSDate timeIntervalSinceReferenceDate];
for(id url in urls) {
    NSImage *img = [[NSImage alloc] initWithContentsOfURL:url];
    if(!img)
        continue;

    NSImage *thumbImg = [self thumbImageFromImage:img];

    [self presentImage:thumbImg];

    NSString *dt = [NSString stringWithFormat:@"%0.1fs",
                   [NSDate timeIntervalSinceReferenceDate]-t0];

    NSLog(@"Presented %@ at %@ sec",url,dt);
}

The thumbImageFromImage method does just what you'd think (creates a smaller image). The code for presentImage is as follows:

- (void)presentImage:(NSImage *)image
{
    CGRect superlayerBounds = view.layer.bounds;
    NSPoint center = NSMakePoint(CGRectGetMidX(superlayerBounds), CGRectGetMidY(superlayerBounds));

    NSRect imageBounds = NSMakeRect(0, 0, image.size.width, image.size.height);

    CGPoint randomPoint = CGPointMake(CGRectGetMaxX(superlayerBounds)*(double)random()/(double)RAND_MAX, CGRectGetMaxY(superlayerBounds)*(double)random()/(double)RAND_MAX);

    CAMediaTimingFunction *tf = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];

    CABasicAnimation *posAnim = [CABasicAnimation animation];
    posAnim.fromValue = [NSValue valueWithPoint:center];
    posAnim.duration = 1.5;
    posAnim.timingFunction = tf;

    CABasicAnimation *bdsAnim = [CABasicAnimation animation];
    bdsAnim.fromValue = [NSValue valueWithRect:NSZeroRect];
    bdsAnim.duration = 1.5;
    bdsAnim.timingFunction = tf;

    CALayer *layer = [CALayer layer];
    layer.contents = image;
    layer.actions = [NSDictionary dictionaryWithObjectsAndKeys:posAnim,@"position", bdsAnim, @"bounds", nil];

    [CATransaction begin];
    [view.layer addSublayer:layer];
    layer.position = randomPoint;
    layer.bounds = NSRectToCGRect(imageBounds);
    [CATransaction commit];
}

The observed result is as if the [CATransaction begin] and [CATransaction commit] calls are bracketing the for loop rather than the call to addSublayer inside presentImage. In fact, I do observe the same all-at-once behavior if I remove the [CATransaction begin] and [CATransaction commit] from inside presentImage and instead bracket the for loop. Actually, I observe the same all-at-once behavior if I remove calls to [CATransaction begin] and [CATransaction commit] altogether.


Solution

  • Good question. What you're seeing is to be expected, given the way the exercise is architected. The reason is that the animation (and drawing) of layers is done on the main thread, which is currently occupied loading images and creating thumbnails. It's not until all of the images have been loaded and the thumbnails have been created that the main thread can actually get around to performing the animation you requested. CATransaction doesn't have much of any impact in this single-threaded context.

    We improve upon this in the next chapter (34, Concurrency) by preparing the images in a background thread (using NSOperationQueue). This frees the main thread to display the new images and perform the animation as each image is loaded and resized in the background.