Why is my CATransaction not honoring the duration I set? - objective-c

I'm porting an iPhone app to Mac OS X. This code was being used successfully on the iPhone:
- (void) moveTiles:(NSArray*)tilesToMove {
[UIView beginAnimations:#"tileMovement" context:nil];
[UIView setAnimationDuration:0.1];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:#selector(tilesStoppedMoving:finished:context:)];
for( NSNumber* aNumber in tilesToMove ) {
int tileNumber = [aNumber intValue];
UIView* aView = [self viewWithTag:tileNumber];
aView.frame = [self makeRectForTile:tileNumber];
}
[UIView commitAnimations];
}
The Mac version uses CATransaction to group the animations, like so:
- (void) moveTiles:(NSArray*)tilesToMove {
[CATransaction begin];
[CATransaction setAnimationDuration:0.1];
[CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
[CATransaction setCompletionBlock:^{
[gameDelegate tilesMoved];
}];
for( NSNumber* aNumber in tilesToMove ) {
int tileNumber = [aNumber intValue];
NSView* aView = [self viewWithTag:tileNumber];
[[aView animator] setFrame:[self makeRectForTile:tileNumber]];
}
[CATransaction commit];
}
The animation is executing fine, except that the duration is 1.0 seconds. I can change the setAnimationDuration: call to anything, or omit it completely, and still the animation is 1.0 seconds in duration, every time. I also don't think the setAnimationTimingFunction: call is doing anything. However, setCompletionBlock: is working, because that block is executing when the animation completes.
What am I doing wrong here?

If I am not mistaken you cannot use CoreAnimation to animate NSView's directly. For that you need NSAnimationContext and [NSView animator]. CATransaction will only work with CALayers.

It doesn't answer the question exactly, but I ended up using NSAnimationContext instead of CATransaction.
- (void) moveTiles:(NSArray*)tilesToMove {
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration:0.1f];
for( NSNumber* aNumber in tilesToMove ) {
int tileNumber = [aNumber intValue];
NSView* aView = [self viewWithTag:tileNumber];
[[aView animator] setFrame:[self makeRectForTile:tileNumber]];
CAAnimation *animation = [aView animationForKey:#"frameOrigin"];
animation.delegate = self;
}
[NSAnimationContext endGrouping];
}
It's works, but I'm not terribly happy about it. Mainly, NSAnimationContext doesn't have a callback completion mechanism like CATransaction does, so I had to put the thing there to explicitly get the view's animation and set the delegate so a callback gets triggered. Problem with that is, it gets triggered multiple times for each animation. This turns out to have no ill effects for what I'm doing, it just feels wrong.
This is workable, but if anyone knows a better solution, I'd still like one.

Related

gap between keyboard and textview during animation

I want to adjust the height of a textview when the keyboard appears. In iOS 7 this can be done by adjusting the NSLayoutConstraint between the textview and the bottomLayoutGuide of the view controller.
That works fine with the code below except for one detail. During the animation the textview runs ahead of the keyboard and a wide gap appears. Similar during the keyboardWillHide method.
The cause for this "bug" is probably because the keyboard starts from the very bottom of the screen while the textview starts higher up due to the height of the toolbar.
Any suggestions on how to fix this?
-(void) keyboardWillShow:(NSNotification *) notification
{
//update constraints
CGRect keyboardFrame = [[[notification userInfo] valueForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGRect convertedKeyboardFrame = [[self view] convertRect:keyboardFrame
fromView:nil];
CGRect toolbarFrame = [[[self navigationController] toolbar] frame];
CGRect convertedToolbarFrame = [[self view] convertRect:toolbarFrame
fromView:nil];
CGFloat toolbarAdjustment = (UIInterfaceOrientationIsPortrait([self interfaceOrientation])) ? CGRectGetHeight(convertedToolbarFrame) :CGRectGetWidth(convertedToolbarFrame);
[[_textView bottomSpaceConstraint] setConstant:CGRectGetHeight(convertedKeyboardFrame) - toolbarAdjustment];
//animate change
for (UIView *view in [[self view] subviews])
{
[view setNeedsUpdateConstraints];
}
[UIView animateWithDuration:[[[notification userInfo] valueForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]
delay:0 //0.2 as possible hack, otherwise a gap appears between keyboard and textview
options:[[[notification userInfo] valueForKey:UIKeyboardAnimationCurveUserInfoKey] integerValue]
animations:^{
for (UIView *view in [[self view] subviews])
{
[view layoutIfNeeded];
}
}
completion:NULL];
}
The delay is probably caused by calling layoutIfNeeded repeatedly in a loop.
In the animation block, just send layoutIfNeeded to the root view, i.e., self.view. Sending layoutIfNeeded to the root view will take care of the entire view hierarchy. So get rid of the loop.
I question if the call to setNeedsUpdateConstraints is necessary; if it is, it only needs to be sent to the root view.
Also, try eliminating the animation options parameter
options:0

how to spin an image continuously

I have an wheel image in .png format, I want to know how can i animate so that it rotates continuously, I searched on stackoverflow and found certain snippets of code which helped me rotate my image but it wouldn't rotate continuously, it would just rotate for a few seconds and stop, the code as follows
the code in viewdidload
UIImageView *imageToMove =
[[UIImageView alloc] initWithImage:[UIImageimageNamed:#"horo_circle.png"]];
[self.view addSubview:imageToMove];
[self rotateImage:imageToMove duration:5.0
curve:UIViewAnimationCurveEaseIn degrees:180];
and the animation
- (void)rotateImage:(UIImageView *)image duration:(NSTimeInterval)duration
curve:(int)curve degrees:(CGFloat)degrees
{
// Setup the animation
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:duration];
[UIView setAnimationCurve:curve];
[UIView setAnimationBeginsFromCurrentState:YES];
// The transform matrix
CGAffineTransform transform =
CGAffineTransformMakeRotation(DEGREES_TO_RADIANS(degrees));
image.transform = transform;
// Commit the changes
[UIView commitAnimations];
}
and the following lines after the import
#define M_PI 3.14159265358979323846264338327950288 /* pi */
#define DEGREES_TO_RADIANS(angle) (angle / 180.0 * M_PI)
You are better of doing this with a CABasicAnimation:
if ([self.spinnerOverlay animationForKey:#"SpinAnimation"] == nil) {
CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
animation.fromValue = [NSNumber numberWithFloat:0.0f];
animation.toValue = [NSNumber numberWithFloat: 2*M_PI];
animation.duration = 10.0f;
animation.repeatCount = INFINITY;
[self.spinnerOverlay.layer addAnimation:animation forKey:#"SpinAnimation"];
}
In this code I check whether the animation is all ready set, not need to set it again.
The spinnerOverlay is in your case the UIImageView you want to rotate.
To stop the animation:
[self.spinnerOverlay.layer removeAnimationForKey:#"SpinAnimation"];
The code bellow will spin a UIImageView called iconView continiously until rotate == NO.
The image will always return to its original position.
- (void)performRotationAnimated
{
[UIView animateWithDuration:0.5
delay:0
options:UIViewAnimationOptionCurveLinear
animations:^{
self.iconView.transform = CGAffineTransformMakeRotation(M_PI);
}
completion:^(BOOL finished){
[UIView animateWithDuration:0.5
delay:0
options:UIViewAnimationOptionCurveLinear
animations:^{
self.iconView.transform = CGAffineTransformMakeRotation(0);
}
completion:^(BOOL finished){
if (_rotate) {
[self performRotationAnimated];
}
}];
}];
}
This is a variation on the same theme.
(Total duration: 2 * 0.5 = 1 sec for a spin)
Works like magic!
Here is another way to do it with a timer.
In your viewController.h file, you'll need to declare your timer and image:
#interface ViewController : UIViewController
{
IBOutlet UIImageView *spinningImage;
NSTimer *spinTimer;
}
Next, in your ViewController.m, you add this code and can make the spin go faster by lowering the time interval or by increasing the transform interval.
-(void) viewDidLoad {
spinTimer = [NSTimer scheduledTimerWithTimeInterval:.01 target: self selector:#selector(spinVoid) userInfo:nil repeats:YES];
}
-(void) spinVoid {
spinningImage.transform = CGAffineTransformRotate(spinningImage.transform, 0.001);
}
Hope this helps!
Now that we're at iOS6, please consider switching to block based animations (that are available since iOS 4.0) rather than the classical method. Try this:
[UIView animateWithDuration:duration delay:0.0 options:UIViewAnimationOptionRepeat animations:^{
image.transform = CGAffineTransformMakeRotation(DEGREES_TO_RADIANS(degrees));
} completion:nil];
Using the method you've chosen for animations :
[UIView setAnimationRepeatCount:10000000];
or, you can specify the UIViewAnimationOptionRepeat flag in the blocks method animateWithDuration:delay:options:animations:completion:

Objective-C: Adding a delay

I'm trying to add a short delay to my app. It's just the splash screen fade out so it won't affect any other functionality in the app (since nothing else is running). I've tried a variety of approaches with no success. (This is happening in viewDidLoad of a viewController):
C's sleep:
...
//add the splash screen
[self.view addSubview:splashScreen];
sleep(3);
[self fadeOut:splashScreen];
NSObject's performSelector (thought this would work because doesn't UIViewController inherit from NSObject?)
[self performSelector:#selector(fadeOut:) afterDelay:3];
NSTimeInterval:
//wait 3 seconds
NSTimeInterval theTimeInterval = 3;
[self fadeOut:splashScreen withADelayOf:&theTimeInterval];
Here is fadeOut (written to work with the NSTimeInterval example)
- (void) fadeOut:(UIView *)viewToToggle withADelayOf:(NSTimeInterval* ) animDelay {
[UIView setAnimationDelay:*animDelay];
[UIView animateWithDuration:0.25 animations:^{
viewToToggle.alpha = 0.0;
}];
}
I get the fadeOut but not the delay. Can someone nudge me in the right direction. Thanks.
you can try dispatch_after or animateWithDuration:delay:options:animations:completion:
double delayInSeconds = 0.5;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
//yourcode
});
or
[UIView animateWithDuration:0.175 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
//your code
}completion:^(BOOL completed){
//animation completion execution
}];
You should rely on this method if you want to animate some properties of your view with some delay:
+(void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion
Have a look to the ref doc here.
So your code could be something like this:
[UIView animateWithDuration:0.25 delay:3.0 options: UIViewAnimationOptionCurveLinear animations:^{
viewToToggle.alpha = 0.0;
} completion: nil];
Tiguero and J2TheC both pointed me in the exact direction I needed to go:
Here's the code I used in case someone else needs the assist:
//add the splash screen
[self.view addSubview:splashScreen];
//fade it out with a delay
[UIView animateWithDuration:0.75 delay:3.0 options:UIViewAnimationOptionCurveEaseIn
animations:^{
splashScreen.alpha = 0.0;
}
completion:^ (BOOL finished) {
//do something on end
}];
Do One thing:
Make an uiimageview ref in the Appdelegate and in the didfinishLunching do this:
// Dont assign the main viewcontroller to the windwo here.
{
img = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"splash.png"]];
img.frame = CGRectMake(0, 0, 320, 480);
[self.window addSubview:img];
[NSTimer scheduledTimerWithTimeInterval:3 target:self selector:#selector(removeSplash) userInfo:nil repeats:NO];
[self.window makeKeyAndVisible];
}
- (void)removeSplash{
[img removerFromSuperview];
self.window.rootViewController = self.viewController;
}

Can't get [[id Animator] setAlphaValue: 0.0] to work

I'm trying to animate an opacity fade out + frame size change of an NSImageView . The frameSize works smoothly but for some reason the alpha value just jumps directly to it's final value at the beginning of the animation. I've tried back layering the superview and I'm running out of ideas here. Here is my code sample ->
AABLCLogo -> NSImageView IBOutlet
speed -> 1.0f
//setting target frame for image view
NSRect newFrame = [AABLCLogo frame];
newFrame.origin.x = newFrame.origin.x + (newFrame.size.width*0.1);
newFrame.origin.y = newFrame.origin.y + (newFrame.size.height*0.1);
newFrame.size.width = newFrame.size.width * 0.8;
newFrame.size.height = newFrame.size.height * 0.8;
//backing layer for super view
[[AABLCLogo superview] setWantsLayer:YES];
//animating image view
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration: speed];
[[AABLCLogo animator] setFrame: newFrame];
[[AABLCLogo animator] setAlphaValue: 0.0];
[NSAnimationContext endGrouping];
//XCode 4 delay snippet
double delayInSeconds = speed;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW,
delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime,
dispatch_get_main_queue(),
^(void) {
//code executes after ''speed'' seconds
[[AABLCLogo superview] setWantsLayer:NO];
[[self view] removeFromSuperview];
});
I suspect this is an issue with animating on the main thread. In most cases, if you want to animate two things simultaneously, you'll need to use NSViewAnimation. Use this code as a guide, it resizes the window while fading out part of it's content.
NSRect newWindowFrame = NSMakeRect(self.window.frame.origin.x, self.window.frame.origin.y, 640, self.window.frame.size.height);
NSMutableArray *viewAnimations = [NSMutableArray array];
NSDictionary *windowSizeDict = [NSDictionary dictionaryWithObjectsAndKeys:self.window, NSViewAnimationTargetKey, [NSValue valueWithRect:newWindowFrame], NSViewAnimationEndFrameKey, nil];
[viewAnimations addObject:windowSizeDict];
NSDictionary *animateOutDict = [NSDictionary dictionaryWithObjectsAndKeys:self.dataSidebarController.containerView, NSViewAnimationTargetKey, NSViewAnimationFadeOutEffect, NSViewAnimationEffectKey, nil];
[viewAnimations addObject:animateOutDict];
NSViewAnimation *animation = [[NSViewAnimation alloc] initWithViewAnimations:viewAnimations];
[animation setDuration:0.35];
[animation setAnimationBlockingMode:NSAnimationNonblocking];
[animation setDelegate:self];
[animation startAnimation];
You want to send setWantsLayer:YES to the view you are animating, not its superview.
NSView's setAlphaValue: only works an a layer-backed or layer-hosting instance, since it forwards the value to the opacity property of NSView's layer.
For the full story, read the "Discussion" of NSView's setAlphaValue: in Apple's documentation.
I believe the layer you request is not create until the next time the view is drawn.
Thus you either need to force display of the superview or wait until the next iteration of the run loop before adding on animations.

combine multiple CAAnimation sequentially

I was reading about CATransactions and then thought this might help solve my problem.
This is what I wan't to do:
I have 3 animations in the same layer, which all have their own duration. I create the animations using CAKeyframeAnimation with a CGMutablePathRef.
say for example:
anim1 -> duration 5s
anim2 -> 3s
anim3 -> 10s
Now I want to serialize them sequentially. I tried to use CAAnimationGroup but the animations run concurrently.
I read about CATransaction, is it a possible solution? Can you give me a little example?
thanks for help !
If by serialization you mean starting each animation after the previous one has finished, use the beginTime property (defined in CAMediaTiming protocol). Note that its documentation is a little misleading. Here's an example:
anim2.beginTime = anim1.beginTime + anim1.duration;
anim3.beginTime = anim2.beginTime + anim2.duration;
If You are sure to do this thing with Layers then you may try like following
Using Completion Block in CATransactions
-(void)animateThreeAnimationsOnLayer:(CALayer*)layer animation:(CABasicAnimation*)firstOne animation:(CABasicAnimation*)secondOne animation:(CABasicAnimation*)thirdOne{
[CATransaction begin];
[CATransaction setCompletionBlock:^{
[CATransaction begin];
[CATransaction setCompletionBlock:^{
[CATransaction begin];
[CATransaction setCompletionBlock:^{
//If any thing on completion of all animations
}];
[layer addAnimation:thirdOne forKey:#"thirdAnimation"];
[CATransaction commit];
}];
[layer addAnimation:secondOne forKey:#"secondAnimation"];
[CATransaction commit];
}];
[layer addAnimation:firstOne forKey:#"firstAnimation"];
[CATransaction commit];
}
Another way is by applying delay to begin animation.
-(void)animateThreeAnimation:(CALayer*)layer animation:(CABasicAnimation*)firstOne animation:(CABasicAnimation*)secondOne animation:(CABasicAnimation*)thirdOne{
firstOne.beginTime=0.0;
secondOne.beginTime=firstOne.duration;
thirdOne.beginTime=firstOne.duration+secondOne.duration;
[layer addAnimation:firstOne forKey:#"firstAnim"];
[layer addAnimation:secondOne forKey:#"secondAnim"];
[layer addAnimation:thirdOne forKey:#"thirdAnim"];
}
And if You are going to use UIVIew Animation
//if View is applicable in your requirement then you can look this one;
-(void)animateThreeAnimationOnView{
[UIView animateWithDuration:2.0 animations:^{
//first Animation
} completion:^(BOOL finished) {
[UIView animateWithDuration:2.0 animations:^{
//Second Animation
} completion:^(BOOL finished) {
[UIView animateWithDuration:2.0 animations:^{
//Third Animation
}];
}];
}];
}