Cannot see a CABasicAnimation when called from within a for loop - ios

I have an animation which randomly moves a tile on a grid which works fine. However when I call in a for loop you dont see any of the animation. How do i get this to work?
for (i=0; i<10 ; i++){
// Call animation function
sleep(.5);
}
Animation looks like
CABasicAnimation *move;
[move setFromValue:[NSValue valueWithCGPoint:[button.layer position]]];
CGPoint toLoc = [button.layer position];
// modify toLoc by +/-70px in x/y direction
[move setToValue:[NSValue valueWithCGPoint:toLoc]];
[move setDuration:0.5];
[button.layer setPosition:toLoc];
I appreciate any advice/comments : )

When you start an animation, you need to let control return back to the system in order to see it perform the animation. Inserting a sleep is almost never the right thing to do in today's world of multi-threaded systems. It is certainly the reason that you are experiencing this issue.
Instead, you might consider doing something like this:
for (i=0; i<10 ; i++){
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(i * 0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// Call animation function
});
}
Given that your sleep time is the same as your intended animation duration, it looks like you're just trying to repeat your animation, though. Instead, try just adding this line and removing the loop:
[move setRepeatCount:10];
You'll also need to instantiate the object and actually add the animation to the layer.
CABasicAnimation *move = [[CABasicAnimation alloc] init];
[move setFromValue:[NSValue valueWithCGPoint:[button.layer position]]];
CGPoint toLoc = [button.layer position];
// modify toLoc by +/-70px in x/y direction
[move setToValue:[NSValue valueWithCGPoint:toLoc]];
[move setDuration:0.5];
[move setRepeatCount:10];
[button.layer addAnimation:move forKey:#"position"];

I think the repetition logic should be handled by the animation function you are using.
If you are using CAAnimation you can use it's callback
"- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag"
to check if you should and then start a new animation or not from there.

Related

Core Animation short hand causes odd behaviour [duplicate]

Here's some relevant code inside a UIView subclass:
- (void) doMyCoolAnimation {
CABasicAnimation* anim = [CABasicAnimation animationWithKeyPath:#"position.x"];
anim.duration = 4;
[self.layer setValue:#200 forKeyPath:anim.keyPath];
[self.layer addAnimation:anim forKey:nil];
}
- (CGFloat) currentX {
CALayer* presLayer = self.layer.presentationLayer;
return presLayer.position.x;
}
When I use [self currentX] while the animation is running, I get 200 (the end value) rather than a value between 0 (the start value) and 200. And yes, the animation is visible to the user, so I'm really confused here.
Here's the code where I call doMyCoolAnimation:, as well as currentX after 1 second.
[self doMyCoolAnimation];
CGFloat delay = 1; // 1 second delay
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
NSLog(#"%f", [self currentX]);
});
Any ideas?
I don't know where the idea for using KVC setters in animation code came from, but that's what the animation itself is for. You're basically telling the layer tree to immediately update to the new position with this line:
[self.layer setValue:#200 forKeyPath:anim.keyPath];
Then wondering why the layer tree won't animate to that position with an animation that has no starting or ending values. There's nothing to animate! Set the animation's toValue and fromValue as appropriate and ditch the setter. Or, if you wish to use an implicit animation, keep the setter, but ditch the animation and set its duration by altering the layer's speed.
My UIView's layer's presentationLayer was not giving me the current values. It was instead giving me the end values of my animation.
To fix this, all I had to do was add...
anim.fromValue = [self.layer valueForKeyPath:#"position.x"];
...to my doMyCoolAnimation method BEFORE I set the end value with:
[self.layer setValue:#200 forKeyPath:#"position.x"];
So in the end, doMyCoolAnimation looks like this:
- (void) doMyCoolAnimation {
CABasicAnimation* anim = [CABasicAnimation animationWithKeyPath:#"position.x"];
anim.duration = 4;
anim.fromValue = [self.layer valueForKeyPath:anim.keyPath];
[self.layer setValue:#200 forKeyPath:anim.keyPath];
[self.layer addAnimation:anim forKey:nil];
}
The way you are creating your animation is wrong, as CodaFi says.
Either use an explicit animation, using a CABasicAnimation, or use implicit animation by changing the layer's properties directly and NOT using a CAAnimation object. Don't mix the two.
When you create a CABasicAnimation object, you use setFromValue and/or setToValue on the animation. Then the animation object takes care of animating the property in the presentation layer.

UIView.layer.presentationLayer returns final value (rather than current value)

Here's some relevant code inside a UIView subclass:
- (void) doMyCoolAnimation {
CABasicAnimation* anim = [CABasicAnimation animationWithKeyPath:#"position.x"];
anim.duration = 4;
[self.layer setValue:#200 forKeyPath:anim.keyPath];
[self.layer addAnimation:anim forKey:nil];
}
- (CGFloat) currentX {
CALayer* presLayer = self.layer.presentationLayer;
return presLayer.position.x;
}
When I use [self currentX] while the animation is running, I get 200 (the end value) rather than a value between 0 (the start value) and 200. And yes, the animation is visible to the user, so I'm really confused here.
Here's the code where I call doMyCoolAnimation:, as well as currentX after 1 second.
[self doMyCoolAnimation];
CGFloat delay = 1; // 1 second delay
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
NSLog(#"%f", [self currentX]);
});
Any ideas?
I don't know where the idea for using KVC setters in animation code came from, but that's what the animation itself is for. You're basically telling the layer tree to immediately update to the new position with this line:
[self.layer setValue:#200 forKeyPath:anim.keyPath];
Then wondering why the layer tree won't animate to that position with an animation that has no starting or ending values. There's nothing to animate! Set the animation's toValue and fromValue as appropriate and ditch the setter. Or, if you wish to use an implicit animation, keep the setter, but ditch the animation and set its duration by altering the layer's speed.
My UIView's layer's presentationLayer was not giving me the current values. It was instead giving me the end values of my animation.
To fix this, all I had to do was add...
anim.fromValue = [self.layer valueForKeyPath:#"position.x"];
...to my doMyCoolAnimation method BEFORE I set the end value with:
[self.layer setValue:#200 forKeyPath:#"position.x"];
So in the end, doMyCoolAnimation looks like this:
- (void) doMyCoolAnimation {
CABasicAnimation* anim = [CABasicAnimation animationWithKeyPath:#"position.x"];
anim.duration = 4;
anim.fromValue = [self.layer valueForKeyPath:anim.keyPath];
[self.layer setValue:#200 forKeyPath:anim.keyPath];
[self.layer addAnimation:anim forKey:nil];
}
The way you are creating your animation is wrong, as CodaFi says.
Either use an explicit animation, using a CABasicAnimation, or use implicit animation by changing the layer's properties directly and NOT using a CAAnimation object. Don't mix the two.
When you create a CABasicAnimation object, you use setFromValue and/or setToValue on the animation. Then the animation object takes care of animating the property in the presentation layer.

Why does this animation only work once?

I have a UIView containing a UITapGestureRecognizer that triggers a method called handleLeftTap.
-(void)handleLeftTap {
self.player.direction = LEFT;
if(!self.isAnimating && self.toTile.isWalkable) {
for(UIView *currentView in self.subviews) {
CABasicAnimation *moveAnimation = [CABasicAnimation animationWithKeyPath:#"position"];
[moveAnimation setDuration:MOVE_ANIMATION_DURATION];
[moveAnimation setDelegate:self];
[moveAnimation setRemovedOnCompletion:NO];
[moveAnimation setFillMode:kCAFillModeForwards];
moveAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(currentView.center.x+TILE_WIDTH, currentView.center.y)];
[currentView.layer addAnimation:moveAnimation forKey:nil];
}
NSLog(#"animated\n");
}
}
When I tap on the screen for the first time, this animation works perfectly; every subview moves to the right by TILE_WIDTH pixels. However, if I tap the screen again, the animation doesn't work at all; no view moves. I stepped through this code with breakpoints and verified that this animation is being added to the layers. It's just that the animation isn't being applied or something like that. Is there a way to fix this?
Not quite sure, but are you sure that the second animation is a different location than the location after the first animation?

Animating CAEmitterLayer's emitterPosition

I am trying to animate a CAEmitterLayer's emitterPosition like this:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"emitterPosition.x"] ;
animation.toValue = (id) toValue ;
animation.removedOnCompletion = NO ;
animation.duration = self.translationDuration ;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut] ;
animation.completion = ^(BOOL finished)
{
[self animateToOtherSide] ;
} ;
[_emitterLayer addAnimation:animation forKey:#"emitterPosition"] ;
CGPoint newEmitterPosition = CGPointMake(toValue.floatValue, self.bounds.size.height/2.0) ;
_emitterLayer.emitterPosition = newEmitterPosition ;
Note that animation.completion is declared in a category that just calls the corresponding CAAnimation delegate method.
The problem is that this doesn't animate at all and shows the emitter in its final position. I was under the impression that once you add the animation to the layer, you should change the actual model behind it to its final position so that when the animation completes the model is in its final state; i.e., to prevent the animation from "snapping back" to its original position.
I have tried placing the last two lines in the animation.completion block, and this does indeed animate as expected. However, when the animation finishes some particles are intermittently emitted at the emitter's original position. If you put the system under load (for example, scrolling a tableview while the animation is playing), this happens more often.
Another solution I was thinking about is to not move the emitterPosition at all but just move the CAEmitterLayer itself, although I haven't tried that yet.
Thanks in advance for any help you can provide.
Perhaps emitterPosition.x is not a valid key path for animation. Try using emitterPosition instead (and so you'll have to provide CGPoint values wrapped up in an NSValue).
I just tried this on my own machine and it works fine:
CABasicAnimation* ba = [CABasicAnimation animationWithKeyPath:#"emitterPosition"];
ba.fromValue = [NSValue valueWithCGPoint:CGPointMake(30,100)];
ba.toValue = [NSValue valueWithCGPoint:CGPointMake(200,100)];
ba.duration = 6;
ba.autoreverses = YES;
ba.repeatCount = HUGE_VALF;
[emit addAnimation:ba forKey:nil];
Other things to think about:
You can typically use nil as the key in addAnimation:forKey:, unless you're going to need to find this animation later (e.g. to remove it or override it in some way). The key path is the important thing.
Setting removedOnCompletion to NO is almost always wrong and is typically the last refuge of a scoundrel (i.e. due to not understanding how animation works).
If, as you say, setting _emitterLayer.emitterPosition = newEmitterPosition inside the completion block does animate, then when why are you using CABasicAnimation at all? Why not just call UIView animate... and set the emitterPosition in the animations block? If that works, it will kill two birds with one stone, moving the position and animating it too.

How do I chain a sequence of animations of the same property on three different objects?

I have been trying to create a sequence of animations that deal three cards from a poker deck sequentially. I just want to animate the position of the three cards -- say the first animation begins immediately on the first card's position and goes for 0.4 seconds, the second begins after 0.4 seconds with the same duration, and the last begins after 0.8 seconds. I can't figure out how to do this! The code below doesn't work. Perhaps I need to use a CAGroupAnimation, but I don't know how to make a group of sequential
animations on the same property!
CGFloat beginTime = 0.0;
for (Card *c in cards) {
CardLayer *cardLayer = [cardToLayerDictionary objectForKey:c];
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"position"];
anim.fromValue = [NSValue valueWithCGPoint:stockLayer.position];
anim.toValue = [NSValue valueWithCGPoint:wasteLayer.position];
anim.duration = 0.4;
anim.beginTime = beginTime;
beginTime += 0.4;
cardLayer.position = wasteLayer.position;
[cardLayer addAnimation:anim forKey:#"position"];
…
}
Like Einstein said, time is relative. All your layers' beginTimes are relative to the timespace of their superlayer---since it isn't animating, they all end up the same.
It looks like there are two possible solutions:
Get an absolute timebase and set each layer's animation's beginTime relative to it:
int i = 0; float delay = 0.4;
for (Card *c in cards) {
// ...
float baseTime = [cardLayer convertTime:CACurrentMediaTime() fromLayer:nil];
anim.beginTime = baseTime + (delay * i++);
// ...
}
Wrap each card's animation in a group. For some reason I don't quite get, this puts those animations on a common timescale, even though these groups are separate from one another. It's worked for some, but I'm a bit disinclined to trust it---seems like spooky action at a distance.
Either way, you're probably also going to want the layers to stay where they are at the end of the animation. You could make the animation "stick" like so:
anim.fillMode = kCAFillModeForwards;
anim.removedOnCompletion = NO;
But that might give your trouble later---the layer's model position is still where it was to start, and if you adjust that after the animation (say, for dragging a card around) you might get weird results. So it might be appropriate to wrap your card dealing animation in a CATransaction, on which you set a completion block that sets the layers' final positions.
Speaking of CATransactions with completion blocks, you could use those to make implicit animations happen in sequence (if you're not using explicit animation for some other reason):
[CATransaction begin];
[CATransaction setCompletionBlock:^{
[CATransaction begin];
[CATransaction setCompletionBlock:^{
card3Layer.position = wasteLayer.position;
}];
card2Layer.position = wasteLayer.position;
[CATransaction end];
}];
card1Layer.position = wasteLayer.position;
[CATransaction end];
The following recursive method is the best I could do to chain animations. I use a CATransaction to animate the position property and set up a block that makes a recursive call for the next card.
-(void)animateDeal:(NSMutableArray*)cardLayers {
if ([cardLayers count] > 0) {
CardLayer *cardLayer = [cardLayers objectAtIndex:0];
[cardLayers removeObjectAtIndex:0];
// ...set new cardLayer.zPosition with animations disabled...
[CATransaction begin];
[CATransaction setCompletionBlock:^{[self animateDeal:cardLayers];}];
[CATransaction setAnimationDuration:0.25];
[cardLayer setFaceUp:YES];
cardLayer.position = wasteLayer.position;
[CATransaction commit];
}
}
Of course this doesn't solve the chaining problem with overlapping time intervals.

Resources