How to Animate a Path Forwards and Backwards? - ios

I have animated a UIBezier Path to go around a circle and change its stroke color whilst doing so. When it goes all the way around, I would like it go all the way around in the opposite direction.
Here is the code that makes it go forwards:
[CATransaction begin];
{
[CATransaction setAnimationDuration: 3.0];//Dynamic Duration
[CATransaction setCompletionBlock:^{
[tutorialCircle removeFromSuperlayer];
tutorialCircle.opacity = 0.0;
strokePart.opacity = 0.0;
[strokePart removeFromSuperlayer];
}];
const double portion = 1.0 / ((double)3);
for (NSUInteger i = 0; i < 3; i++)
{
strokePart = [[CAShapeLayer alloc] init];
strokePart.fillColor = [[UIColor clearColor] CGColor];
strokePart.frame = tutorialCircle.bounds;
strokePart.path = tutorialCircle.path;
strokePart.lineCap = tutorialCircle.lineCap;
strokePart.lineWidth = tutorialCircle.lineWidth;
if (i == 0) {
strokePart.strokeColor = [UIColor greenColor].CGColor;
}
else if (i == 1) {
strokePart.strokeColor = [UIColor orangeColor].CGColor;
}
else if (i == 2) {
strokePart.strokeColor = [UIColor redColor].CGColor;
}
strokePart.strokeStart = i * portion;
strokePart.strokeEnd = (i + 1) * portion;
[tutorialCircle addSublayer: strokePart];
animation = [CAKeyframeAnimation animationWithKeyPath: #"strokeEnd"];
NSArray* times = #[ #(0.0), // Note: This works because both the times and the stroke start/end are on scales of 0..1
#(strokePart.strokeStart),
#(strokePart.strokeEnd),
#(1.0) ];
NSArray* values = #[ #(strokePart.strokeStart),
#(strokePart.strokeStart),
#(strokePart.strokeEnd),
#(strokePart.strokeEnd) ];
animation.keyTimes = times;
animation.values = values;
animation.removedOnCompletion = YES;
animation.fillMode = kCAFillModeForwards;
[animation setDelegate:self];
[strokePart addAnimation: animation forKey: #"whatever"];
}
}
[CATransaction commit];
So in my animationDidStop delegate method for the first animation, I know I'd be calling the second animation to go backwards, however, I am struggling to create the animation for this to go backwards.

Have you tried?
animation.autoreverses = YES

Related

CAShapeLayer animation doesn't stay on screen but disappears

I'm trying to draw a animated circle but every segment needs to have another color. Now everything works except that my piece that is just drawed before I call the method again disappears so only the last part stays. I don't want that, I want that after 4 times a whole circle is drawn in different color strokes.
How can I fix this that it doesn't disappears?
This is my init code:
- (void)_initCircle {
_circle = [CAShapeLayer layer];
_radius = 100;
// Make a circular shape
_circle.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 2.0 * _radius, 2.0 * _radius) cornerRadius:_radius].CGPath;
// Center the shape in self.view
_circle.position = CGPointMake(CGRectGetMidX(self.view.frame) - _radius,
CGRectGetMidY(self.view.frame) - _radius);
_circle.fillColor = [UIColor clearColor].CGColor;
_circle.lineWidth = 5;
// Add to parent layer
[self.view.layer addSublayer:_circle];
}
This is my draw piece:
- (void)_drawStrokeOnCircleFrom:(float)start to:(float)end withColor:(UIColor *)color {
// Configure the apperence of the circle
_circle.strokeColor = color.CGColor;
_circle.strokeStart = start;
_circle.strokeEnd = end;
[CATransaction begin];
// Configure animation
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = 2.0; // "animate over 10 seconds or so.."
drawAnimation.repeatCount = 1.0; // Animate only once..
// Animate from no part of the stroke being drawn to the entire stroke being drawn
drawAnimation.fromValue = [NSNumber numberWithFloat:start];
drawAnimation.toValue = [NSNumber numberWithFloat:end];
// Experiment with timing to get the appearence to look the way you want
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[CATransaction setCompletionBlock:^{
NSLog(#"Complete animation");
_index++;
if(_index < _votes.count){
_startStroke = _endStroke;
_endStroke += [_votes[_index] floatValue] / _totalListens;
[self _drawStrokeOnCircleFrom:_startStroke to:_endStroke withColor:_colors[_index]];
}
}];
// Add the animation to the circle
[_circle addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
[CATransaction commit];
}
Try setting these values on your CABasicAnimation:
drawAnimation.removedOnCompletion = NO;
drawAnimation.fillMode = kCAFillModeForwards;
That worked for me using "kCAFillModeBoth", both statements are necessary:
//to keep it after finished
animation.removedOnCompletion = false;
animation.fillMode = kCAFillModeBoth;
I solved the problem with extra layers like this:
- (void)_drawStrokeOnCircle {
[self _initCircle];
[CATransaction begin];
for (NSUInteger i = 0; i < 4; i++)
{
CAShapeLayer* strokePart = [[CAShapeLayer alloc] init];
strokePart.fillColor = [[UIColor clearColor] CGColor];
strokePart.frame = _circle.bounds;
strokePart.path = _circle.path;
strokePart.lineCap = _circle.lineCap;
strokePart.lineWidth = _circle.lineWidth;
// Configure the apperence of the circle
strokePart.strokeColor = ((UIColor *)_colors[i]).CGColor;
strokePart.strokeStart = [_segmentsStart[i] floatValue];
strokePart.strokeEnd = [_segmentsEnd[i] floatValue];
[_circle addSublayer: strokePart];
// Configure animation
CAKeyframeAnimation* drawAnimation = [CAKeyframeAnimation animationWithKeyPath: #"strokeEnd"];
drawAnimation.duration = 4.0;
// Animate from no part of the stroke being drawn to the entire stroke being drawn
NSArray* times = #[ #(0.0),
#(strokePart.strokeStart),
#(strokePart.strokeEnd),
#(1.0) ];
NSArray* values = #[ #(strokePart.strokeStart),
#(strokePart.strokeStart),
#(strokePart.strokeEnd),
#(strokePart.strokeEnd) ];
drawAnimation.keyTimes = times;
drawAnimation.values = values;
drawAnimation.removedOnCompletion = NO;
drawAnimation.fillMode = kCAFillModeForwards;
// Experiment with timing to get the appearence to look the way you want
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[strokePart addAnimation: drawAnimation forKey: #"drawCircleAnimation"];
}
[CATransaction commit];
}
- (void)_initCircle {
[_circle removeFromSuperlayer];
_circle = [CAShapeLayer layer];
_radius = 100;
// Make a circular shape
_circle.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 2.0 * _radius, 2.0 * _radius) cornerRadius:_radius].CGPath;
// Center the shape in self.view
_circle.position = CGPointMake(CGRectGetMidX(self.view.frame) - _radius,
CGRectGetMidY(self.view.frame) - _radius);
_circle.fillColor = [UIColor clearColor].CGColor;
_circle.lineWidth = 5;
// Add to parent layer
[self.view.layer addSublayer:_circle];
}
SWIFT 4, Xcode 11:
let strokeIt = CABasicAnimation(keyPath: "strokeEnd")
let timeLeftShapeLayer = CAShapeLayer()
and in your viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
drawTimeLeftShape()
strokeIt.fillMode = CAMediaTimingFillMode.forwards
strokeIt.isRemovedOnCompletion = false
strokeIt.fromValue = 0
strokeIt.toValue = 1
strokeIt.duration = 2
timeLeftShapeLayer.add(strokeIt, forKey: nil)
}
and by UIBezierPathcreate function to draw circle:
func drawTimeLeftShape() {
timeLeftShapeLayer.path = UIBezierPath(
arcCenter: CGPoint(x: myView.frame.midX , y: myView.frame.midY),
radius: myView.frame.width / 2,
startAngle: -90.degreesToRadians,
endAngle: 270.degreesToRadians,
clockwise: true
).cgPath
timeLeftShapeLayer.strokeColor = UIColor.red.cgColor
timeLeftShapeLayer.fillColor = UIColor.clear.cgColor
timeLeftShapeLayer.lineWidth = 1
timeLeftShapeLayer.strokeEnd = 0.0
view.layer.addSublayer(timeLeftShapeLayer)
}

How do I smoothly update the toValue/end point of an animation in Core Animation?

I'm trying to emulate the effect Apple has as a progress indicator when downloading an app in iOS 7, somewhat of a pie chart:
My code's coming along pretty well. I can give it a value to go to and it will update to that.
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
CGFloat radius = CGRectGetWidth(self.frame) / 2;
CGFloat inset = 1;
CAShapeLayer *ring = [CAShapeLayer layer];
ring.path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(self.bounds, inset, inset)cornerRadius:radius-inset].CGPath;
ring.fillColor = [UIColor clearColor].CGColor;
ring.strokeColor = [UIColor whiteColor].CGColor;
ring.lineWidth = 2;
self.innerPie = [CAShapeLayer layer];
inset = radius/2;
self.innerPie.path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(self.bounds, inset, inset)
cornerRadius:radius-inset].CGPath;
self.innerPie.fillColor = [UIColor clearColor].CGColor;
self.innerPie.strokeColor = [UIColor whiteColor].CGColor;
self.innerPie.lineWidth = (radius-inset)*2;
[self.layer addSublayer:ring];
[self.layer addSublayer:self.innerPie];
self.progress = 0.0;
self.innerPie.hidden = YES;
}
- (void)setProgress:(CGFloat)progress animated:(BOOL)animated {
if (animated) {
self.innerPie.hidden = NO;
self.innerPie.strokeEnd = progress;
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
pathAnimation.duration = 0.5;
pathAnimation.fromValue = [NSNumber numberWithFloat:self.progress];
pathAnimation.toValue = [NSNumber numberWithFloat:progress];
[self.innerPie addAnimation:pathAnimation forKey:#"strokeEnd"];
}
else {
[CATransaction setDisableActions:YES];
[CATransaction begin];
self.innerPie.hidden = NO;
self.innerPie.strokeEnd = progress;
[CATransaction commit];
}
self.progress = progress;
}
My issue lies in updating the progress while in the middle of an animation. For example, if I'm downloading a file and the progress jumps from 0.1 to 0.2 (10% to 20%), I want to animate that. Say I give it a 0.5 second animation. What if, 0.2 seconds into the 0.5 second animation it jumps to 0.4 (40%)?
With my current code, it jumps to where it should have ended (when it should have animated) in the first animation (20%) and then starts animating again toward 40%.
What I'd like is for it to almost update the toValue to 0.4. At least that's what I think I want. Conceptually I'd like it to continue the animation toward the new value without interrupting the previous value. Smoothness, to put it simply.
How would I accomplish this?
I know there's libraries to do this. I want to do it myself for learning purposes.
You should replace
pathAnimation.fromValue = [NSNumber numberWithFloat:self.progress];
with :
pathAnimation.fromValue = [NSNumber numberWithFloat:[self.innerPie.presentationLayer strokeEnd]];
Initially, [self.innerPie.presentationLayer strokeEnd]]will be 1, so you will need to set the initial value to 0. Add these 2 lines to place where you create your 'innerPie' :
self.innerPie.strokeStart = 0;
self.innerPie.strokeEnd = 0;
I've tested and it works.

Why when I set a low duration value for my CABasicAnimation does it jump?

Sample Project: http://cl.ly/1W3V3b0D2001
I'm using CABasicAnimation to create a progress indicator that is like a pie chart. Similar to the iOS 7 app download animation:
The animation is set up as follows:
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
CGFloat radius = CGRectGetWidth(self.frame) / 2;
CGFloat inset = 1;
CAShapeLayer *ring = [CAShapeLayer layer];
ring.path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(self.bounds, inset, inset)
cornerRadius:radius-inset].CGPath;
ring.fillColor = [UIColor clearColor].CGColor;
ring.strokeColor = [UIColor whiteColor].CGColor;
ring.lineWidth = 2;
self.innerPie = [CAShapeLayer layer];
inset = radius/2;
self.innerPie.path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(self.bounds, inset, inset)
cornerRadius:radius-inset].CGPath;
self.innerPie.fillColor = [UIColor clearColor].CGColor;
self.innerPie.strokeColor = [UIColor whiteColor].CGColor;
self.innerPie.lineWidth = (radius-inset)*2;
self.innerPie.strokeStart = 0;
self.innerPie.strokeEnd = 0;
[self.layer addSublayer:ring];
[self.layer addSublayer:self.innerPie];
self.progress = 0.0;
}
The animation is triggered by setting the progress of the view:
- (void)setProgress:(CGFloat)progress animated:(BOOL)animated {
self.progress = progress;
if (animated) {
CGFloat totalDurationForFullCircleAnimation = 0.25;
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
self.innerPie.strokeEnd = progress;
pathAnimation.delegate = self;
pathAnimation.fromValue = #([self.innerPie.presentationLayer strokeEnd]);
pathAnimation.toValue = #(progress);
pathAnimation.duration = totalDurationForFullCircleAnimation * ([pathAnimation.toValue floatValue] - [pathAnimation.fromValue floatValue]);
[self.innerPie addAnimation:pathAnimation forKey:#"strokeEndAnimation"];
}
else {
[CATransaction setDisableActions:YES];
[CATransaction begin];
self.innerPie.strokeEnd = progress;
[CATransaction commit];
}
}
However, in cases where I set the progress to something small, such as 0.25, there's a jump in the animation. It goes a little forward clockwise, jumps back, then keeps going forward as normal. It's worth nothing that this does not happen if the duration or progress is set higher.
How do I stop the jump? This code works well in every case except when the progress is very low. What am I doing wrong?
Ah! We should have seen this earlier. The problem is this line in your if (animated) block:
self.innerPie.strokeEnd = progress;
Since innerPie is a Core Animation layer, this causes an implicit animation (most property changes do). This animation is fighting with your own animation. You can prevent this from happening by disabling implicit animation while setting the strokeEnd:
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.innerPie.strokeEnd = progress;
[CATransaction commit];
(Note how setDisableActions: is within the begin/commit.)
Alternatively, you can remove your own animation and just use the automatic one, using something like setAnimationDuration: to change its length.
Original Suggestions:
My guess is that your drawRect: is being called, which resets your strokeEnd value. Those layers should probably not be set up in drawRect: anyway. Try moving that setup to an init method or didMoveToWindow: or similar.
If that's not effective, I would suggest adding log statements to track the value of progress and [self.innerPie.presentationLayer strokeEnd] each time the method is called; perhaps they're not doing what you expect.
Why are you using drawRect:? There should be no drawRect: method in your class.
You should not be using drawRect if you are also using self.layer. Use one or the other, never both.
Change it to something like:
- (id)initWithFrame:(CGRect)frame
{
if (!(self = [super initWithFrame:frame])
return nil;
CGFloat radius = CGRectGetWidth(self.frame) / 2;
CGFloat inset = 1;
CAShapeLayer *ring = [CAShapeLayer layer];
ring.path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(self.bounds, inset, inset)
cornerRadius:radius-inset].CGPath;
ring.fillColor = [UIColor clearColor].CGColor;
ring.strokeColor = [UIColor whiteColor].CGColor;
ring.lineWidth = 2;
self.innerPie = [CAShapeLayer layer];
inset = radius/2;
self.innerPie.path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(self.bounds, inset, inset)
cornerRadius:radius-inset].CGPath;
self.innerPie.fillColor = [UIColor clearColor].CGColor;
self.innerPie.strokeColor = [UIColor whiteColor].CGColor;
self.innerPie.lineWidth = (radius-inset)*2;
self.innerPie.strokeStart = 0;
self.innerPie.strokeEnd = 0;
[self.layer addSublayer:ring];
[self.layer addSublayer:self.innerPie];
self.progress = 0.0;
return self;
}
When travelling within same distance, the smaller the duration, the higher the speed. When you set the duration too small, the speed will be very high, and that's why it looks like jumping.

Loop animation - doesn't go

I'm trying to repeat a sequence of animations, inside a LOOP, changing at each loop some parameters randomly. Here is the code. Anyone please knows why it doesn't work?
If I call it once with a button action, it works, but with a loop it doesn't.
Thanks a lot! Giuseppe
-(IBAction)startLoop:(id)sender {
for (int i=1;i<10; i++) {
[self animation2];
}
}
-(id) animation2 {
int max=500;
UIImage *myImage = [UIImage imageNamed:#"coccinella2.png"];
CALayer *myLayer = [CALayer layer];
myLayer.contents = (id)myImage.CGImage;
myLayer.bounds = CGRectMake(0, 0, 50, 60);
[myLayer setPosition:CGPointMake(arc4random()%(max), arc4random()%(max))];
[myLayer setBounds:CGRectMake(0.0, 0.0, 50.0, 60.0)];
[self.view.layer addSublayer:myLayer];
//translation1
CGPoint startPt = CGPointMake(arc4random()%(max),arc4random()%(max));
CGPoint endPt = CGPointMake(arc4random()%(max),arc4random()%(max));
CABasicAnimation *transl1 = [CABasicAnimation animationWithKeyPath:#"position"];
transl1.removedOnCompletion = FALSE;
transl1.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
transl1.fromValue = [NSValue valueWithCGPoint:startPt];
transl1.toValue = [NSValue valueWithCGPoint:endPt];
transl1.duration = 2.0;
transl1.fillMode = kCAFillModeForwards;
transl1.beginTime = 0;
//scale 1
CABasicAnimation *scale1 = [CABasicAnimation
animationWithKeyPath:#"transform.scale"];
scale1.removedOnCompletion = FALSE;
[scale1 setToValue:[NSNumber numberWithInt:3]];
[scale1 setDuration:2.0f];
scale1.fillMode = kCAFillModeForwards;
scale1.beginTime = 0;
//rotation1
CABasicAnimation *rotation1 = [CABasicAnimation
animationWithKeyPath:#"transform.rotation.z"];
rotation1.removedOnCompletion = FALSE;
[rotation1 setFromValue:DegreesToNumber(0)];
[rotation1 setToValue:DegreesToNumber(90)];
//rotation1.repeatCount = HUGE_VALF;
[rotation1 setDuration:2.0f];
rotation1.fillMode = kCAFillModeForwards;
rotation1.beginTime = 0;
//group
CAAnimationGroup* group = [CAAnimationGroup animation];
[group setDuration: 6.0];
group.removedOnCompletion = FALSE;
group.fillMode = kCAFillModeForwards;
[group setAnimations: [NSArray arrayWithObjects:scale1, transl1, rotation1, nil]];
[myLayer addAnimation: group forKey: nil];
}
Your code doesn't repeat the annotation 10 times but starts 10 animations right away. If your goal is to start the animations after the previous one ended you should try using an NSTimer.
You can use an NSTimer to trigger each group of animations at a different time, or you can set the beginTime on each group, with code like this:
group.beginTime = CACurrentMediaTime() + delay;
For animations, using the beginTime gives more accurate timing, since CA is run on a separate thread and doesn't stall like NSTimers if your app gets busy.

Synchronising image, text, and positioning with CoreAnimation

I am a bit of a beginner with animations and have been experimenting with CoreAnimation for a couple of days. Feel free to warn me if this question does not make sense, but I'm trying to achieve the following. I have three objects:
one should be an image, moving according to a given pattern
one should be an UIImage that swaps two images
one should be a text (CATextLayer?) whose content changes
The three actions should happen in sync.
As an example, think about a programme showing a sinusoid function, like in a ECG, oscillating between -1 and +1: the first image would then move according to the current value (-1, -0.9, -0.8, ... 0, +0.1, +0.2, ... 1), the swap image would show "+" for positive values and "-" for negative values, and the text would alternate between "Positive" and "Negative".
I've tried with CAAnimationGroup but I'm clearly missing something. Some code:
{
// image that moves
CALayer *img1 = [CALayer layer];
img1.bounds = CGRectMake(0, 0, 20, 20);
img1.position = CGPointMake(x1,y1);
UIImage *img1Image = [UIImage imageNamed:#"image.png"];
img1.contents = (id)img1Image.CGImage;
// image that changes
CALayer *swap = [CALayer layer];
swap.bounds = CGRectMake(0, 0, 30, 30);
swap.position = CGPointMake(x2,y2);
NSString* nameswap = #"img_swap_1.png"
UIImage *swapImg = [UIImage imageNamed:nameswap];
// text
CATextLayer *text = [CATextLayer layer];
text.bounds = CGRectMake(0, 0, 100, 100);
text.position = CGPointMake(x3,y3);
text.string = #"Text";
// create animations
CGFloat duration = 0.2;
CGFloat totalDuration = 0.0;
CGFloat start = 0;
NSMutableArray* animarray = [[NSMutableArray alloc] init];
NSMutableArray* swapanimarray = [[NSMutableArray alloc] init];
NSMutableArray* textanimarray = [[NSMutableArray alloc] init];
float prev_x = 0;
float prev_y = 0;
// I get my values for moving the object
for (NSDictionary* event in self.events) {
float actual_x = [[event valueForKey:#"x"] floatValue];
float actual_y = [[event valueForKey:#"y"] floatValue];
// image move animation
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"position"];
CGPoint startPt = CGPointMake(prev_x,prev_y);
CGPoint endPt = CGPointMake(actual_x, actual_y);
anim.duration = duration;
anim.fromValue = [NSValue valueWithCGPoint:startPt];
anim.toValue = [NSValue valueWithCGPoint:endPt];
anim.beginTime = start;
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[animarray addObject:anim];
// image swap animation
CABasicAnimation *swapanim = [CABasicAnimation animationWithKeyPath:#"contents"];
swapanim.duration = duration;
swapanim.beginTime = start;
NSString* swapnamefrom = [NSString stringWithFormat:#"%#.png", prev_name];
NSString* swapnameto = [NSString stringWithFormat:#"%#.png", current_name];
UIImage *swapFromImage = [UIImage imageNamed:swapnamefrom];
UIImage *swapToImage = [UIImage imageNamed:swapnameto];
swapanim.fromValue = (id)(swapFromImage.CGImage);
swapanim.toValue = (id)(swapToImage.CGImage);
swapanim.fillMode = kCAFillModeForwards;
swapanim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[swapanimarray addObject:swapanim];
//text animation
CABasicAnimation *textanim = [CABasicAnimation animationWithKeyPath:#"contents"];
textanim.duration = duration;
textanim.fromValue = #"Hey";
textanim.toValue = #"Hello";
textanim.beginTime = start;
[textanimarray addObject:textanim];
// final time settings
prev_x = actual_x;
prev_y = actual_y;
start = start + duration;
totalDuration = start + duration;
}
}
CAAnimationGroup* group = [CAAnimationGroup animation];
[group setDuration:totalDuration];
group.removedOnCompletion = NO;
group.fillMode = kCAFillModeForwards;
[group setAnimations:animarray];
CAAnimationGroup* swapgroup = [CAAnimationGroup animation];
[swapgroup setDuration:totalDuration];
swapgroup.removedOnCompletion = NO;
swapgroup.fillMode = kCAFillModeForwards;
[swapgroup setAnimations:swapanimarray];
CAAnimationGroup* textgroup = [CAAnimationGroup animation];
[textgroup setDuration:totalDuration];
textgroup.removedOnCompletion = NO;
textgroup.fillMode = kCAFillModeForwards;
[textgroup setAnimations:textanimarray];
[ball addAnimation:group forKey:#"position"];
[swap addAnimation:flaggroup forKey:#"position"];
[text addAnimation:textgroup forKey:#"contents"];
[self.layer addSublayer:ball];
[self.layer addSublayer:swap];
[self.layer addSublayer:text];
}
Now... the problems:
1) the swap image reverts to the original at every swap. So, if I swap A->B, I see it going from A to B in the expected duration time, but then reverting to A. I've read a number of threads on SO about this but couldn't get it to work.
2) changing the string of the text layer in a timed fashion... is this possible with this infrastructure? Basically, I'm trying to get the text and the swap image to change as soon as the first image moves, as described in the example.
3) setting the delegate for the CABasicAnimation doesn't have any effect, although it does for the CAAnimationGroup: as a result, you can't manage events like animationDidStop for every single animation, just for the whole group. Is there any alternative way to do so?
4) following from 3), is it possible, using CAAnimationGroup, to intercept the events to create a stop/start behaviour? Let's suppose I wanted to have play/stop buttons, and to resume the animation from exactly where I had left it?
As a conclusive question, I would simply like to know if anyone did something similar and, most importantly, if this way of doing things (using a CAAnimationGroup) is actually the way to go or if it's better to use CAKeyFrameAnimation or something else.
I managed to solve the problem, albeit with some workarounds.
1) The trick here is that
removedOnCompletion = NO;
fillMode = kCAFillModeForwards;
need to be assigned to every single animation, not to the CAAnimationGroup.
2) This requires a separate animation to be determined for the CATextLayer, and animating the "string" property. In this case, the code was right except for the "contents" key-value which should have been
[text addAnimation:textgroup forKey:#"contentAnimate"];
Alternatively, see point 3. The callback way of operation allows to change a label.text.
3) The only way to do this is to actually not use the CAAnimationGroup and set up a sequence of CABasicAnimation. The general schema is: create the first CABasicAnimation in a function, assign the delegate. In the animationDidStop method, add a callback to the function that creates the CABasicAnimation. This way, each animation will be created. At the same time, this allows to intercept and react to specific events within the animation.
-(void)performNextAnimation:(int)index {
// ... this gives for granted you have an object for #index ...
NSDictionary* thisEvent = [self.events objectAtIndex:index];
float prev_x = [thisEvent valueForKey:#"prev_x"];
float prev_y = [thisEvent valueForKey:#"prev_x"];
float actual_x = [thisEvent valueForKey:#"prev_x"];
float actual_y = [thisEvent valueForKey:#"prev_x"];
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"position"];
GPoint startPt = CGPointMake(prev_x,prev_y);
CGPoint endPt = CGPointMake(actual_x, actual_y);
anim.duration = 0.5;
anim.fromValue = [NSValue valueWithCGPoint:startPt];
anim.toValue = [NSValue valueWithCGPoint:endPt];
anim.beginTime = start;
[anim setDelegate:self];
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[object addAnimation:anim forKey:#"position"];
}
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag {
// ... your conditions go here ...
// ... your actions go here ...
// e.g. set label.text
[self performNextAnimation];
}
4) Following from 3, this is not possible in CAAnimationGroup. Reading the docs, I would say that CAAnimationGroup is not intended to be used for sequences in which each event represents a step forward in time (for this you need to use CAKeyFrameAnimation or a sequence of CABasicAnimation with a callback function, as I did). CAAnimationGroup is intended for linked animation that should be executed on an all-or-nothing basis (similarly to CATransaction).

Resources