iPhone - UINavigationController - custom animation with CATransaction - animation

I am trying to create a category for UINavigationController so I can put together a few custom animations. I came across this question and it got me started. I've come up with the following for a push function:
- (void)pushViewControllerMoveInFromBottom:(UIViewController *)viewController {
[CATransaction begin];
CATransition *transition;
transition = [CATransition animation];
transition.type = kCATransitionMoveIn;
transition.subtype = kCATransitionFromBottom;
transition.duration = 0.7;
[CATransaction setValue:(id)kCFBooleanTrue
forKey:kCATransactionDisableActions];
[[[[self.view subviews] objectAtIndex:0] layer] addAnimation:transition forKey:nil];
[self pushViewController:viewController animated:YES];
[CATransaction commit];
}
I don't understand which view/layer is being accessed via the subview call. So if someone could help me understand that, that would be helpful. It seems to be effecting whatever view is about to come onto the screen.
But my main question is that I'm trying to create a pop that will "undo" the above function. For clarity sake, lets say I have two views, A and B. A is the first view that is shown on the screen. The function above, will move in B without pushing A out. Much like a modal view would. But the function below slides A back in over B, rather that sliding B up and leaving A in place.
- (void)popViewControllerMoveInFromTop {
[CATransaction begin];
CATransition *transition;
transition = [CATransition animation];
transition.type = kCATransitionMoveIn;
transition.subtype = kCATransitionFromTop;
transition.duration = 0.7;
[CATransaction setValue:(id)kCFBooleanTrue
forKey:kCATransactionDisableActions];
[[[[self.view subviews] objectAtIndex:0] layer] addAnimation:transition forKey:nil];
[self popViewControllerAnimated:YES];
[CATransaction commit];
}
I don't want to use a modal view and I would rather not use custom animation blocks to change the frame of the views.

I've accomplished something similar, though I didn't bother to move it into it's own function:
CATransition *transition = [CATransition animation];
transition.duration = 0.5;
transition.type = kCATransitionMoveIn;
transition.subtype = kCATransitionFromBottom;
[self.navigationController.view.layer addAnimation:transition forKey:nil];
[self.navigationController popViewControllerAnimated:NO]
You could abstract it out. Using your code, it would look like:
- (void)popViewControllerMoveInFromTop {
[CATransaction begin];
CATransition *transition;
transition = [CATransition animation];
transition.type = kCATransitionMoveIn;
transition.subtype = kCATransitionFromTop;
transition.duration = 0.7;
[CATransaction setValue:(id)kCFBooleanTrue
forKey:kCATransactionDisableActions];
[self.view.layer addAnimation:transition forKey:nil];
[self popViewControllerAnimated:NO];
[CATransaction commit];
}
You can also create a custom transition for pop using UIView's animations. Here you are applying an animation to the view that's about to disappear and then popping it from the view stack:
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.75];
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromRight forView:self.navigationController.view cache:NO];
[self.navigationController popViewControllerAnimated:NO];
[UIView commitAnimations];

Related

Animating and removing an NSImageView object failed?

My app situation:
I am now having an app displaying a dynamic picture. And I want to capture its current image and save it to disc.
My goal:
I am working on an animated visual effect: add an image view on top, and then animates it from its origin frame into CGRecrZero. Finally, remove this image view from vie hierarchy after animation is done.
Now problem:
First time triggerring the action, it seems work. But when trigger it at the second time, the image view does not animate and stay atop of all subviews.
Codes of the view controller(with ARC):
Firstly, this is the IBAction to trigger the snapshot:
- (IBAction)actionSaveSnapshot:(id)sender
{
... // Save the image file. This works everytime.
NSThread *tempThread = [[NSThread alloc] initWithTarget:self
selector:#selector(threadSimulateScreenCapture:)
object:nil];
if (tempThread)
{
[tempThread start];
tempThread = nil; // Simply release it
}
}
And here is the thread:
- (void)threadSimulateScreenCapture:(id)arg
{#autoreleasepool {
NSImageView __strong *subImageView;
subImageView = [[NSImageView alloc] init];
NSRect subRect = [_imageView frame]; // the dynamic image view which I want to snapshot with
const CGFloat animationTime = 0.4;
[subImageView setWantsLayer:YES];
[subImageView setImageScaling:NSImageScaleAxesIndependently];
[subImageView setImage:[_imageView image]];
dispatch_async(dispatch_get_main_queue(), ^{
[CATransaction begin];
[self.view addSubview:subImageView];
[subImageView setFrame:subRect];
[subImageView setNeedsDisplay:YES];
[CATransaction commit];
});
[NSThread sleepForTimeInterval:0.01];
dispatch_async(dispatch_get_main_queue(), ^{
[CATransaction begin];
[CATransaction setAnimationDuration:animationTime];
[subImageView.layer setFrame:CGRectZero];
[CATransaction commit];
});
[NSThread sleepForTimeInterval:(NSTimeInterval)animationTime]; // wait for end of animation
dispatch_async(dispatch_get_main_queue(), ^{
[subImageView removeFromSuperview];
});
NSLog(#"SubView Count: %#\n%#", self.view.subviews, subImageView); // MARK. described below
subImageView = nil; // release the image
}} // ENDS: thread and aotureleasepool
Every time the program runs to the "MARK" line and print the subview array, it is clear thar the subImageView is not removed from superview then. And every time the thread ends, a "deleted thread with uncommitted CATransaction ...." raised.
What wrong did I do?
I may solved my own problem: At the line with [subImageView setFrame:subRect];, insert another line with: [subImageView.layer setFrame:subRect];
Meanwhile, add another line before the "addSubview:" method: [self.view setWantsLayer:YES];.
It seemed work. But I do not know why. Why I should set both view and layer's frame?

NSView Does Not Respond To Mouse Events After CABasicAnimation

I have a pageLeft and pageRight animation that animates the position and alpha of 2 views, making a total of 4 simultaneous CABasicAnimations:
Move next page into the view
Move current page out of the view
Fade in next page
Fade out current page
Everything works well with the animations and setup. this question is about mouse events after the page animation. I have found that after the animation runs, the nextView does not receive mouseDown: events. If the animation is not run because the shouldNotAnimate block is run below instead, the nextView does receive mouseDown: events.
This means that something about the way I have setup my CABasicAnimations and CATransaction is causing this. I am stumped as to what it could be. Any ideas?
Animation Code:
BOOL forward = currentIndex < index;
if (shouldNotAnimate) {
// handles swapping pages faster than the animation duration
[self.sourceViewContainer setSubviews:[NSArray array]];
[nextView.layer removeAllAnimations];
[nextView setFrame:self.sourceViewContainer.bounds];
[nextView.layer setPosition:self.sourceViewContainer.bounds.origin];
[nextView.layer setOpacity:1.0];
[self.sourceViewContainer addSubview:nextView];
[currentView removeFromSuperview];
[currentView.layer removeAllAnimations];
[currentView setFrame:self.sourceViewContainer.bounds];
[currentView.layer setPosition:self.sourceViewContainer.bounds.origin];
[currentView.layer setOpacity:1.0];
self.animationsRunning = NO;
} else {
self.animationsRunning = YES;
[CATransaction begin];
[CATransaction setCompletionBlock:^{
self.animationsRunning = NO;
}];
// Setup incoming push animation
[nextView setFrame:CGRectMake(self.sourceViewContainer.bounds.size.width * (forward ? 1 : -1), 0, self.sourceViewContainer.bounds.size.width, self.sourceViewContainer.bounds.size.width)];
[self.sourceViewContainer addSubview:nextView];
CABasicAnimation *incomingAnimation = [CABasicAnimation animationWithKeyPath:#"position"];
incomingAnimation.fromValue = [NSValue valueWithPoint:nextView.frame.origin];
incomingAnimation.toValue = [NSValue valueWithPoint:self.sourceViewContainer.bounds.origin];
incomingAnimation.duration = PANEL_PAGE_SWIPE_ANIMATION_DURATION;
incomingAnimation.removedOnCompletion = NO;
incomingAnimation.fillMode = kCAFillModeForwards;
incomingAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
incomingAnimation.completion = ^(BOOL finished) {
[nextView.layer setPosition:self.sourceViewContainer.bounds.origin];
[nextView.layer removeAnimationForKey:#"incoming"];
};
[nextView.layer addAnimation:incomingAnimation forKey:#"incoming"];
// Setup outgoing push animation
CGRect offscreen = CGRectMake(self.sourceViewContainer.bounds.size.width * (forward ? -1 : 1), 0, self.sourceViewContainer.bounds.size.width, self.sourceViewContainer.bounds.size.height);
CABasicAnimation *outgoingAnimation = [CABasicAnimation animationWithKeyPath:#"position"];
outgoingAnimation.fromValue = [NSValue valueWithPoint:self.sourceViewContainer.bounds.origin];
outgoingAnimation.toValue = [NSValue valueWithPoint:offscreen.origin];
outgoingAnimation.duration = PANEL_PAGE_SWIPE_ANIMATION_DURATION;
outgoingAnimation.removedOnCompletion = NO;
outgoingAnimation.fillMode = kCAFillModeForwards;
outgoingAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
outgoingAnimation.completion = ^(BOOL finished) {
[currentView removeFromSuperview];
[currentView.layer setPosition:self.sourceViewContainer.bounds.origin];
[currentView.layer removeAnimationForKey:#"outgoing"];
};
[currentView.layer addAnimation:outgoingAnimation forKey:#"outgoing"];
// Setup incoming alpha animation
CABasicAnimation *fadeInAnimation = [CABasicAnimation animationWithKeyPath:#"opacity"];
fadeInAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
fadeInAnimation.toValue = [NSNumber numberWithFloat:1.0f];
fadeInAnimation.duration = PANEL_PAGE_SWIPE_ANIMATION_DURATION;
fadeInAnimation.removedOnCompletion = NO;
fadeInAnimation.fillMode = kCAFillModeForwards;
fadeInAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
fadeInAnimation.completion = ^(BOOL finished) {
[nextView.layer setOpacity:1.0f];
[nextView.layer removeAnimationForKey:#"fadeIn"];
};
[nextView.layer addAnimation:fadeInAnimation forKey:#"fadeIn"];
// Setup outgoing alpha animation
CABasicAnimation *fadeOutAnimation = [CABasicAnimation animationWithKeyPath:#"opacity"];
fadeOutAnimation.fromValue = [NSNumber numberWithFloat:1.0f];
fadeOutAnimation.toValue = [NSNumber numberWithFloat:0.0f];
fadeOutAnimation.duration = PANEL_PAGE_SWIPE_ANIMATION_DURATION;
fadeOutAnimation.removedOnCompletion = NO;
fadeOutAnimation.fillMode = kCAFillModeForwards;
fadeOutAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
fadeOutAnimation.completion = ^(BOOL finished) {
[currentView removeFromSuperview];
[currentView.layer setOpacity:1.0f];
[currentView.layer removeAnimationForKey:#"fadeOut"];
};
[currentView.layer addAnimation:fadeOutAnimation forKey:#"fadeOut"];
[CATransaction commit];
} // Run animations
Solution
I had to explicitly set the view frame in the completion block, even though I had done this immediately prior to the animation calls.
[CATransaction begin];
[CATransaction setCompletionBlock:^{
self.animationsRunning = NO;
[nextView setFrame:self.sourceViewContainer.bounds];
}];
I think I know what's going on.
CAAnimation objects don't actually change the underlying property that they animation. Instead, it applies the properties to a presentation layer, which is drawn instead of the regular layer. When you create an animation with removedOnCompletion=false, the presentation layer is left active after the animation is completed, but the underlying property still isn't changed. Plus, moving a view's layer doesn't move the view itself, so it won't respond to user interaction at it's new location.
I'd suggest moving your views using UIView block animation (UIView animateWithDuration:completion: and it's relatives) instead of using CAAnimation objects. Those methods actually move the view to it's new position, so that it responds to user interaction at the final location once the animation is complete.
You could do your move and alpha change all in one animation block.
I had an issue with iOS once where the UIMapView because unresponsive after an animation due to a call to the map view. Somehow userInteractionEnable was set to FALSE after the animation finished.
What you could do to test if that is the case is to assert that the value for userInteractionEnable is TRUE.
assert(view.userInteractionEnable != FALSE); // ensure user interaction is enabled
You may find that when you run the app that it breaks on this assertion. If it does, you can simply set it to TRUE and your app will work as you expect.
As for my issue with UIMapView, the problem went away after an iOS version update so it was likely a bug caused by an edge case. I filed a bug about the problem which may have helped raise awareness about the unexpected behavior.

Animation doesn't show

Here is my code snippet for cocoa application using core animation, somehow the animation doesn't show.
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position"];
[animation setDelegate:self];
NSRect pos = [imageView frame];
[animation setFromValue:[NSValue valueWithRect:pos]];
NSPoint point = NSMakePoint(pos.origin.x-40, pos.origin.y);
[animation setToValue:[NSValue valueWithPoint:point]];
[animation setDuration:2.0];
[[imageView animator] addAnimation:animation forKey:#"myTest"];
while this is the working code:
NSRect position = [imageView frame];
position.origin.x -= 40;
[[imageView animator] setFrame:position];
But autoReverse doesn't work.
Anything wrong with the first one? And how to make the reverse movement work in the 2nd one? Thanks!
I don't know for sure, but for the first one, position is a CGPoint, so you might try using that type for your fromValue and toValue. You're currently using an NSRect and an NSPoint (the latter of which should work, but not sure about the former).
For the second, how are you specifying auto-reverse? +setAnimationRepeatAutoreverses needs to be called from inside an animation block (after "beginAnimations")
(Note: I'm not very experienced with Cocoa since I'm chiefly an iOS developer; I haven't tested any of the following stuff)
I think the problem is that you're trying to mix CoreAnimation and Animator Proxy. You don't add the animation to the Animator, but to the layer:
[[imageView layer] addAnimation:animation forKey:#"myTest"];
Another possibility might be to use NSViewAnimation and chain them together. See the Animation Programming Guide for Cocoa, page 13. So you'd have one animation to go in one direction, and once it's finished it triggers the second one that goes back. It seems to work like this:
NSMutableDictionary *firstDict = [NSMutableDictionary dictionary];
[firstDict setObject:imageView forKey:NSViewAnimationTargetKey];
[firstDict setObject:[NSValue valueWithRect:originalFrame] forKey:NSViewAnimationStartFrameKey];
[firstDict setObject:[NSValue valueWithRect:targetFrame] forKey:NSViewAnimationEndFrameKey];
NSMutableDictionary *secondDict = [NSMutableDictionary dictionary];
[secondDict setObject:imageView forKey:NSViewAnimationTargetKey];
[secondDict setObject:[NSValue valueWithRect:targetFrame] forKey:NSViewAnimationStartFrameKey];
[secondDict setObject:[NSValue valueWithRect:originalFrame] forKey:NSViewAnimationEndFrameKey];
NSViewAnimation *firstAnimation = [[NSViewAnimation alloc] initWithViewAnimations:[NSArray arrayWithObject:firstDict]];
[firstAnimation setDuration:2.0];
NSViewAnimation *secondAnimation = [[NSViewAnimation alloc] initWithViewAnimations:[NSArray arrayWithObject:secondDict]];
[secondAnimation setDuration:2.0];
[secondAnimation startWhenAnimation:firstAnimation reachesProgress:1.0];
[firstAnimation startAnimation];
Then, in Lion (OS X 10.7) you can set a completion handler when using Animator Proxy. It should work like this:
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration:2.0];
[[NSAnimationContext currentContext] setCompletionHandler:^(void) {
// Here comes your code for the reverse animation.
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration:2.0];
[[aView animator] setFrameOrigin:originalPosition];
[NSAnimationContext endGrouping];
}];
[[aView animator] setFrameOrigin:position];
[NSAnimationContext endGrouping];

Change animation time for properties of a CALayer

I have a CALayer to animate a change in its image contents. Now, how can I change how long it takes for this animation to take place?
You can just call:
[CATransaction setAnimationDuration:durationSecs]
in -layoutSublayers or anywhere else that you modify the layers and expect them to implicitly animate. This will effect the current implicit transaction and any sub-transactions within this one.
A different way to do this:
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:2.5f] forKey:kCATransactionAnimationDuration];
//Perform CALayer actions, such as changing the layer contents, position, whatever.
aCALayerObject.contents = [self newCALayerContents];
[CATransaction commit];
That code would animate the change of the CALayer's contents over 2.5 seconds. You can also use this to disable all animations completely. Like this:
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
It's more or less simple. You have an ivar CALayer *yourLayer. Then you set the delegate and implement the delegate method -(id<CAAction>)actionForLayer:forKey:
- (void)awakeFromNib {
yourLayer.delegate = self;
yourLayer.name = #"yourLayer";
}
- (id <CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
if([layer.name isEqualToString yourLayer.name]) { // Check for right layer
CABasicAnimation *ani = [CABasicAnimation animationWithKeyPath:event]; // Default Animation for 'event'
ani.duration = .5; // Your custom animation duration
return ani;
} else return nil; // Default Animation
}

Core Animation 'flip' animation

I'm looking to use Core Animation to simulate a flip clock animation in a Mac application. Currently I have three CALayer's representing the top and bottom half of the digit, and a third used to represent the flip animation (a solution found in the follow article: Creating an iPad flip-clock with Core Animation.
The animation of the flip layer is broken into two stages: flipping from the top to the middle of the digit, and then from the middle to the bottom. To achieve this, I use a delegate function which is called whenever an animation ends:
- (void)animationDidStop:(CAAnimation *)oldAnimation finished:(BOOL)flag
{
int digitIndex = [[oldAnimation valueForKey:#"digit"] intValue];
int currentValue = [[oldAnimation valueForKey:#"value"] intValue];
NSMutableArray *digit = [digits objectAtIndex:digitIndex];
CALayer *flipLayer = [digit objectAtIndex:tickerFlip];
CALayer *bottomLayer = [digit objectAtIndex:tickerBottom];
if([[oldAnimation valueForKey:#"state"] isEqual:#"top"] && flag) {
NSLog(#"Top animation finished");
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
flipLayer.contents = [bottomImages objectAtIndex:currentValue];
flipLayer.anchorPoint = CGPointMake(0.0, 1.0);
flipLayer.hidden = NO;
[CATransaction commit];
CABasicAnimation *anim = [self generateAnimationForState:#"bottom"];
[anim setValue:[NSString stringWithFormat:#"%d", digitIndex] forKey:#"digit"];
[anim setValue:[NSString stringWithFormat:#"%d", currentValue] forKey:#"value"];
[flipLayer addAnimation:anim forKey:nil];
} else if([[oldAnimation valueForKey:#"state"] isEqual:#"bottom"] && flag) {
NSLog(#"Bottom animation finished");
// Hide our flip layer
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
bottomLayer.contents = [bottomImages objectAtIndex:currentValue];
flipLayer.hidden = YES;
flipLayer.anchorPoint = CGPointMake(0.0, 0.0);
[CATransaction commit];
}
}
This delegate function makes use of a helper function which generates the transform for the flip layer depending upon its state:
- (CABasicAnimation *)generateAnimationForState:(NSString *)state
{
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"transform"];
anim.duration = 0.15;
anim.repeatCount = 1;
// Check which animation we're doing
if([state isEqualToString:#"top"])
{
anim.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(0.0f, 1, 0, 0)];
anim.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(M_PI/2, 1, 0, 0)];
}
else
{
anim.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(-M_PI/2, 1, 0, 0)];
anim.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(0.0f, 1, 0, 0)];
}
anim.delegate = self;
anim.removedOnCompletion = NO;
// Set our animations state
[anim setValue:state forKey:#"state"];
return anim;
}
This solution works but causes some slight flickering when an animation is in progress. I believe this is due to the transform on my flip layer resetting between the 'top' and 'bottom' animations. It's important to note that after the first stage of the animation completes, I set the flip layers anchor point to the top of the image, ensuring the flip pivots correctly.
Currently I'm unsure if my animation has been setup optimally. I'm new to transformations and Core Animation in general. Can anyone point me in the right direction?
After
anim.removedOnCompletion = NO;
try inserting this code:
anim.fillMode = kCAFillModeForwards;
And let me know. Essentially the code snippet is supposed to prevent the 'reset' that is causing the flicker.

Resources