JefferyRPrice
JefferyRPrice

Reputation: 895

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.

Upvotes: 1

Views: 1501

Answers (2)

JefferyRPrice
JefferyRPrice

Reputation: 895

Well, after letting this bug the hell out of me for a few weeks, here's what seems to make sense, and seems (somewhat) obvious to me now, so hope I'm not too far off.

From the code given in my original question above, the main thread is responsible for all UI updates, including the animations, yet the main thread is effectively blocked from updating the UI until presentImage is done. This is true regardless of how we split presentImage into threads (as in Ch 34).

For example, if I have presentImage called as the target of a button rather than looping on it as in the code above, each animation begins as soon as I click the button and proceeds as I would expect regardless of how rapidly (or not) I click the button. Doing this ends up with multiple simultaneous animations as I would expect (and as I mistakenly expected from the original).

I'm not sure that I've captured the official technical details accurately, but this at least satisfies me as an answer.

Upvotes: 0

Adam Preble
Adam Preble

Reputation: 2222

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.

Upvotes: 3

Related Questions