i want to start a new animation routine as soon as the previous one completes. When the previous does complete however, the new one does not get triggered since the delegate method is not being called. I have delegated a view controller to handle the animations of a button's CALayer. The 'buttonSlide' is the previous animation while 'buttonFade' is the new one.
Here's the code snippet:-
-(void)viewWillAppear:(BOOL)animated{
NSLog(#"TimeView appearing...");
[super viewWillAppear:animated];
[self pressButton:nil]; // Shows current time as soon as TimeView appears
// Silver challenge
CABasicAnimation *buttonSlide= [CABasicAnimation animationWithKeyPath:#"position"];
[buttonSlide setFromValue:[NSValue valueWithCGPoint:CGPointMake(320, 191+15)]];
[buttonSlide setToValue:[NSValue valueWithCGPoint:CGPointMake(160, 191+15)]];
[buttonSlide setDuration:.12];
[button.layer addAnimation:buttonSlide forKey:#"buttonSlide"];
[buttonSlide setDelegate:self];
CABasicAnimation *buttonFade= [CABasicAnimation animationWithKeyPath:#"opacity"];
[buttonFade setFromValue:[NSNumber numberWithFloat:.2]];
[buttonFade setToValue:[NSNumber numberWithFloat:1]];
[buttonFade setDuration:10];
}
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
NSLog(#"%# finshed: %d",anim,flag);
CAAnimation *buttonFade= [button.layer animationForKey:#"opacity"];
[button.layer addAnimation:buttonFade forKey:#"buttonFade"];
}
The delegate method isn't logging to the console as its not even getting called..Please assist.
Put this:
[buttonSlide setDelegate:self];
Before:
[button.layer addAnimation:buttonSlide forKey:#"buttonSlide"];
Thus becomes:
[buttonSlide setDelegate:self];
[button.layer addAnimation:buttonSlide forKey:#"buttonSlide"];
Would work.
Hope this helps.
Related
I was informed recently by meronix that use of beginAnimations is discouraged. Reading through the UIView class reference I see that this is indeed true - according to the Apple class ref:
Use of this method is discouraged in iOS 4.0 and later. You should use
the block-based animation methods to specify your animations instead.
I see that a large number of other methods - which I use frequently - are also "discouraged" which means they'll be around for iOS 6 (hopefully) but probably will be deprecated/removed eventually.
Why are these methods being discouraged?
As a side note, right now I'm using beginAnimations in all sorts of apps, most commonly to move the view up when a keyboard is shown.
//Pushes the view up if one of the table forms is selected for editing
- (void) keyboardDidShow:(NSNotification *)aNotification
{
if ([isRaised boolValue] == NO)
{
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.25];
self.view.center = CGPointMake(self.view.center.x, self.view.center.y-moveAmount);
[UIView commitAnimations];
isRaised = [NSNumber numberWithBool:YES];
}
}
Not sure how to duplicate this functionality with block-based methods; a tutorial link would be nice.
They are discouraged because there is a better, cleaner alternative
In this case all a block animation does is automatically wrap your animation changes (setCenter: for example) in begin and commit calls so you dont forget. It also provides a completion block, which means you don't have to deal with delegate methods.
Apple's documentation on this is very good but as an example, to do the same animation in block form it would be
[UIView animateWithDuration:0.25 animations:^{
self.view.center = CGPointMake(self.view.center.x, self.view.center.y-moveAmount);
} completion:^(BOOL finished){
}];
Also ray wenderlich has a good post on block animations: link
Another way is to think about a possible implementation of block animations
+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations
{
[UIView beginAnimations];
[UIView setAnimationDuration:duration];
animations();
[UIView commitAnimations];
}
Check out this method on UIView, which makes it quite simple. The trickiest part nowadays is not allowing a block to have a strong pointer to self:
//Pushes the view up if one of the table forms is selected for editing
- (void) keyboardDidShow:(NSNotification *)aNotification
{
if ([isRaised boolValue] == NO)
{
__block UIView *myView = self.view;
[UIView animateWithDuration:0.25 animations:^(){
myView.center = CGPointMake(self.view.center.x, self.view.center.y-moveAmount);
}];
isRaised = [NSNumber numberWithBool:YES];
}
}
I have a UIButton subclass that does some custom drawing and animations. That is all working fine and dandy.
However, most of my buttons dismiss the current view via their superview calling [self dismissViewControllerAnimated] once it is confirmed with the model that whatever the button push was supposed to accomplish was actually done, and I want there to be a delay to allow the animation to complete before dismissing the view.
I am able to easily enough animate the UIButton subclass on touchesEnded and then call [super touchesEnded], which works fine except that it doesn't let my animations finish before dismissing the view. Like this:
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
CABasicAnimation *myAnimation = [CABasicAnimation animationWithKeyPath:#"transform.foo"];
//set up myAnimation's properties
[self.layer addAnimation:shakeAnimation forKey:nil];
[super touchesEnded:touches withEvent:event]; //works! but no delay
}
My first attempt at creating a delay was by using CATransaction, as follows:
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
CABasicAnimation *myAnimation = [CABasicAnimation animationWithKeyPath:#"transform.foo"];
//set up myAnimation's properties
[CATransaction begin];
[CATransaction setCompletionBlock:^{
[super touchesEnded:touches withEvent:event]; //doesn't seem to do anything :-/
}];
[self.layer addAnimation:shakeAnimation forKey:nil];
[CATransaction commit];
}
Which, as far as I can tell, is executing CATransaction's completionBlock, but it just isn't doing anything.
I've also tried assigning the touches and event arguments from touchesEnded to both properties and global variables, and then executing [super touchesEnded] in another method called by an NSTimer. The same thing seems to be occurring where the code is executing, but my call to [super touchesEnded] isn't doing anything.
I've dug around online for hours. Added stubs of the other touches methods from UIResponder which just contain [super touches...]. Tried setting up my global variables for the method called by NSTimer differently (I very well may be missing something in regards to global variables...). This button is being created by the Storyboard, but I've set the class to my custom class, so I don't think UIButton's +(UIButton *)buttonWithType method is affecting this.
What am I missing? Is there some small thing I'm forgetting about or is there just no way to delay the call to [super touchesEnded] from a UIButton subclass?
I was not able to solve this, only find a way around it.
My last stab at solving this was to figure out whether the [super touchesEnded...] inside the completion block was being executed in a thread that was different from the thread it was executed in when it was outside the completion block... and no, they both appear to be the main thread (Apple's documentation on CATransaction does state that its completionBlock is always run in the main thread).
So in case someone else is banging their head against this, here's my less-than-elegant solution:
1.) In my subclass of UIButton I created a weak property called containingVC.
2.) In every single (ugh) VC that uses the custom button class I have to do this:
#implemenation VCThatUsesCustomButtonsOneOfWayTooMany
-(void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
self.firstCustomButton.containingVC = self;
self.secondCustomButton.containingVC = self;
self.thirdCustomButton.containingVC = self;
....
self.lastCustomButton.containingVC = self;
//you're probably better off using an IBOutletColletion and NSArray's makeObjectPerformSelector:withObject...
}
#end
3.) Then in my custom UIButton class I have something like this:
-(void)animateForPushDismissCurrView
{
CAAnimation *buttonAnimation = ...make animation
[CATransaction begin];
[CATransaction setCompletionBlock:^{
[self.containingVC performSegueWithIdentifier:#"segueID" sender:self.containingVC];
}];
[self.layer addAnimation:buttonAnimation forKey:nil];
[CATransaction commit];
}
4.) Then in whatever VC that the user is currently interacting with, after making sure the button has done whatever it was supposed to do (in my case it checks with the model to confirm that the relevant change was made), each button has to call [someCustomButton animateForPushDismissCurrView] which then animates the button press and then fires the UIStoryboardSegue that actually dismisses the view.
Obviously this would work for going deeper, not just unwinding, but you'd need additional logic in the custom button's -(void)animateForPush method or a separate method entirely.
Again, if I'm missing something here, I'd love to know what it is. This seems like an absurd number of hoops to jump through to accomplish what seems like a simple task.
Lastly, and most importantly, if it just won't work with the [super touchesEnded...] method in CATransaction's completionBlock, I'd like to know WHY. I suspect that it has something to do with threads or the weirdness that is Objective-C's super keyword.
I have a custom CALayer with an animatable property called progress. The basic code is:
#implementation AnimatedPieChartLayer
#dynamic progress;
+ (BOOL)needsDisplayForKey:(NSString *)key {
return [key isEqualToString: #"progress"] || [super needsDisplayForKey: key];
}
- (id <CAAction>)actionForKey:(NSString *)key {
if ([self presentationLayer] != nil) {
if ([key isEqualToString: #"progress"]) {
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath: key];
[anim setFromValue: [[self presentationLayer] valueForKey: key]];
[anim setDuration: 0.75f];
return anim;
}
}
return [super actionForKey: key];
}
- (void)drawInContext:(CGContextRef)context {
// do stuff
}
#end
In the view controller I do this, to try and get my animation to start from the beginning every time:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear: animated];
[pieChart setProgress: 0.0];
}
This all works perfectly. I put the layer on a view, the view in a view controller, and it all works as expected.
The complication is that my view controller is inside a UIScrollView. This presents a situation where the user could swipe away from my view controller before the CALayer animation completes. If they swipe back quickly, the CALayer animation does something weird. It reverses. It's like the animation is trying to go back to the beginning. Once it gets there it starts over, and animates the way it is supposed to.
So the question is, how can I prevent this. I want the animation to start from the beginning every time the view is displayed – regardless of what happened last time.
Any suggestions?
UPDATE
Here's a visual example of the problem:
Working:
Failing:
In the setup you have there, you have an implicit animation for the key "progress" so that whenever the progress property of the layer changes, it animates. This works both when the value increases and when it decreases (as seen in your second image).
To restore the layer to the default 0 progress state without an animation, you can wrap the property change in a CATransaction where all actions are disabled. This will disable the implicit animation so that you can start over from 0 progress.
[CATransaction begin];
[CATransaction setDisableActions:YES]; // all animations are disabled ...
[pieChart setProgress: 0.0];
[CATransaction commit]; // ... until this line
Probably too simple a suggestion to merit an 'answer,' but I would suggest canceling the animation. Looks like you could rewrite this code pretty easily so that the progress updates were checking some complete flag and then when the view get hidden, kill it. This thread has some good ideas on it.
I have a problem with an animation.
The problem is that if I try to animate a view that is already created all goes well, if I try to create and animate a view at the same time the animation doesn't work.
Can anyone help me?
My Methods
+ (LoginView *)sharedInstance {
#synchronized(self) {
if (nil == _sharedInstance) {
_sharedInstance = (LoginView *)[[[NSBundle mainBundle] loadNibNamed:#"LoginView" owner:nil options:nil] objectAtIndex:0];
}
}
return _sharedInstance;
}
- (void)hide:(BOOL)value animated:(BOOL)animated {
CATransition * animation = [CATransition animation];
animation.type = kCATransitionFade;
[animation setDuration:1.0];
if(_autoManageModalView)
[animation setDelegate:self];
[[self layer] removeAllAnimations];
[[self layer] addAnimation:animation forKey:kCATransition];
self.hidden = value;
}
How I call them
[[LoginView sharedInstance] hide:NO animated:YES];
The first time (with the same call) animation doesn't work, from the secondo time all goes well. Thank in advance!
You are animating your view too early in its lifecycle. In theory, you create a view, then display it somewhere (e.g., addSubview:), then you animate it.
It is highly possible, though I have not checked it, that the first time that your hide:animated: method is called the self.layer property is null; in any case, the animation would happen before the view is displayed, so you would not see it.
All in all, first display the view, then call the hide:animated: method on it.
After your comment: try and call the hide:animated: method through a method like:
performSelector:withObject:afterDelay:
If you specify a 0.0 delay, this will simply queue the call to hide:animate: on the main loop, so that all the processing related to loadNibNamed: can happen and so give your view the time to be set up for display correctly.
In order to use performSelector:withObject:afterDelay: you will need to modify your method signature so that it takes one argument and this must be an NSObject-derived type, not a primitive type.
I am loading ViewControllerA inside ViewController B. It is a small animated character, inside a larger scene.
Inside ViewControllerA, there is a rotation animation like:
CAKeyframeAnimation *bobble = [CAKeyframeAnimation animationWithKeyPath:#"transform.rotation"];
NSArray *times = [NSArray arrayWithObjects:[NSNumber numberWithFloat:0.0],
[NSNumber numberWithFloat:0.25],
[NSNumber numberWithFloat:0.5],
[NSNumber numberWithFloat:0.75],
[NSNumber numberWithFloat:1.0],
nil];
[bobble setKeyTimes:times];
NSArray *values = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:degreesToRadian(0)],
[NSNumber numberWithFloat:degreesToRadian(5)],
[NSNumber numberWithFloat:degreesToRadian(0)],
[NSNumber numberWithFloat:degreesToRadian(-5)],
[NSNumber numberWithFloat:degreesToRadian(0)],
nil];
[bobble setValues:values];
bobble.repeatCount = HUGE_VALF;
bobble.autoreverses = YES;
bobble.duration = 5.0;
bobble.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[self.view.layer addAnimation:bobble forKey:#"transform.rotation"];
It's own viewDidLoad and viewDidAppear look like:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
self.view.center = CGPointMake(640, 201);
[self.view setAnchorPointAndReposition:CGPointMake(.7, .7)];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self bobble];
[NSTimer scheduledTimerWithTimeInterval:3. target:self selector:#selector(blinkFromTimer:) userInfo:nil repeats:YES];
}
In iOS5+ it loads and animates just fine, in iOS 4.3, it loads but no animation ca be seen.
Any insight?
You aren't supposed to host one view controller inside another unless you use the new parent view controller support in iOS 5. Even in iOS 5, using a view controller to manage a small animated character is serious overkill, and probably not a good fit at all. Better to create a custom subclass of UIView and use that.
It is POSSIBLE to host one view controller inside another pre iOS 5, but the burden is on you to make everything work, and you wind up fighting against the OS design every step of the way. I fought that battle early in the days of the iPhone SDK (as it was known at first) and gave up. It's a nightmare from start to finish. I strongly advise against it.
I know of a major software developer (Apple partner level) who's app Apple threatened to take down from the store for doing that.