Reputation: 895
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
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
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