How to synchronize another object with a repeating animation? - ios

I have an animation involving an object continuously bouncing back and forth between two walls with a time period of 2 seconds between two positions given by the CGPoints 'positionStart' and 'positionEnd'. The code doing this is a pretty simple and basic animation:
CABasicAnimation *theAnimation;
theAnimation=[CABasicAnimation animationWithKeyPath:#"position"];
theAnimation.duration=1.0;
theAnimation.beginTime=CACurrentMediaTime()+1;
theAnimation.repeatCount=HUGE_VALF;
theAnimation.autoreverses=YES;
theAnimation.fromValue=[NSValue valueWithCGPoint:positionStart];
theAnimation.toValue=[NSValue valueWithCGPoint:positionEnd];
[self.pulseLayer addAnimation:theAnimation forKey:#"animatePosition"];
Now here's the question: I want to have this section of code send a message to another object with every "tick" of this clock; that is, every time pulseLayer's presented position goes to 'positionStart' I want this section of code to send a target-action message to another object (let's call it Clock #2) which I want to keep in sync with the periodic bouncing of the 'pulseLayer'.
Is there any simple way of doing this? The best alternative ideas I could come up with are (1) to start an NSTimer at the same time as the start of this animation to send "tick" timing pulses to Clock #2, but I worry that although the NSTimer object and the 'pulseLayer' bounce may start off in sync that small timing errors may build up over time so that they will no longer appear to be in sync after a long time. There would certainly be nothing to force them to remain synchronized. Another idea (2) is to do away with this endless bouncing of pulseLayer (i.e., change theAnimation.repeatCount to equal 0) and have another object send timing pulses to start the one-bounce animation every 2 seconds to both this object and to the "Clock #2" object that I want to keep in sync.
Any ideas about the best way to implement what I want to do here?

autoreverse does not send notification
(until all animations completed, which is never in the example above)
Use 2 different animations. It is simple to setup:
Setup:
// To
self.toAnimation = [CABasicAnimation animationWithKeyPath:#"position"];
self.toAnimation.duration=1.0;
self.toAnimation.fromValue=[NSValue valueWithCGPoint:positionStart];
self.toAnimation.toValue=[NSValue valueWithCGPoint:positionEnd];
self.toAnimation.delegate:self;
// And fro
self.froAnimation = [CABasicAnimation animationWithKeyPath:#"position"];
self.froAnimation.duration=1.0;
self.froAnimation.fromValue=[NSValue valueWithCGPoint:positionEnd];
self.froAnimation.toValue=[NSValue valueWithCGPoint:positionStart];
self.froAnimation.delegate:self;
Start:
// Start the chain of animations by adding the "next" (the first) animation
[self toAndFro:self.froAnimation];
Delegate:
- (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)finished {
[self toAndFro:(CABasicAnimation*)animation];
}
Core:
- (void)toAndFro:(CABasicAnimation *)stoppedAnimation {
BOOL wasFro = self.froAnimation == stoppedAnimation;
CABasicAnimation * theAnimation = ((wasFro)
? self.toAnimation
: self.froAnimation);
[self.pulseLayer addAnimation:theAnimation forKey:#"animatePosition"];
// Tick here! You are now in perfect sync
}

Related

Animate a UIView along a part of a bezier path

I'm trying animate a UIView along a portion of a bezier path. I found a way to move the the view to any part of the path using this code:
let animation = CAKeyframeAnimation(keyPath: "position")
animation.path = trackPath.cgPath
animation.rotationMode = kCAAnimationRotateAuto
animation.speed = 0
animation.timeOffset = offset
animation.duration = 1
animation.calculationMode = kCAAnimationPaced
square.layer.add(animation, forKey: "animate position along path")
However, this just moves the view to the desired point and doesn't animate it. How do you animate a view along over a portion of a bezier path?
Thanks
You can accomplish this by modifying the timing of the "complete" animation and a animation group that wraps it.
To illustrate how the timing of such an animation works, imagine – instead of animating a position along a path – that a color is being animated. Then, the complete animation from one color to another can be illustrated as this, where a value further to the right is at a later time — so that the very left is the start value and the very right is the end value:
Note that the "timing" of this animation is linear. This is intentional since the "timing" of the end result will be configured later.
In this animation, imagine that we're looking to animate only the middle third, this part of the animation:
There are a few steps to animating only this part of the animation.
First, configure the "complete" animation to have linear timing (or in the case of an animation along a path; to have a paced calculation mode) and to have a the "complete" duration.
For example: if you're looking to animate a third of the animation an have that take 1 second, configure the complete animation to take 3 seconds.
let relativeStart = 1.0/3.0
let relativeEnd = 2.0/3.0
let duration = 1.0
let innerDuration = duration / (relativeEnd - relativeStart) // 3 seconds
// configure the "complete" animation
animation.duration = innerDuration
This means that the animation currently is illustrated like this (the full animation):
Next, so that the animation "starts" a third of the way into the full animation, we "offset" its time by a third of the duration:
animation.timeOffset = relativeStart * innerDuration
Now the animation is illustrated like this, offset and wrapping from its end value to its start value:
Next, so that we only display part of this animation, we create an animation group with the wanted duration and add only the "complete" animation to it.
let group = CAAnimationGroup()
group.animations = [animation]
group.duration = duration
Even though this group contains an animation that is 3 seconds long it will end after just 1 second, meaning that the 2 seconds of the offset "complete" animation will never be shown.
Now the group animation is illustrated like this, ending after a third of the "complete" animation:
If you look closely you'll see that this (the part that isn't faded out) is the middle third of the "complete" animation.
Now that this group animates animates between the wanted values, it (the group) can be configured further and then added to a layer. For example, if you wanted this "partial" animation to reverse, repeat, and have a timing curve you would configure these things on the group:
group.repeatCount = HUGE
group.autoreverses = true
group.timingFunction = CAMediaTimingFunction(name: "easeInEaseOut")
With this additional configuration, the animation group would be illustrated like this:
As a more concrete example, this is an animation that I created using this technique (in fact all the code is from that example) that moves a layer back and forth like a pendulum. In this case the "complete" animation was a "position" animation along a path that was a full circle
I've done a similar animation just a few days ago:
// I want the animation to go along the circular path of the oval shape
let flightAnimation = CAKeyframeAnimation(keyPath: "position")
flightAnimation.path = ovalShapeLayer.path
// I set this one to make the animation go smoothly along the path
flightAnimation.calculationMode = kCAAnimationPaced
flightAnimation.duration = 1.5
flightAnimation.rotationMode = kCAAnimationRotateAuto
airplaneLayer.add(flightAnimation, forKey: nil)
I see that you set speed to zero and a time offset. Why do you need them?
I would suggest to try the animation using just the parameters in the above code and then try to tune it from them.

Observe UIView frame while animating [duplicate]

I want to observe changes to the x coordinate of my UIView's origin while it is being animated using animateWithDuration:delay:options:animations:completion:. I want to track changes in the x coordinate during this animation at a granular level because I want to make a change in interaction to another view that the view being animated may make contact with. I want to make that change at the exact point of contact. I want to understand the best way to do something like this at a higher level:
-- Should I use animateWithDuration:... in the completion call back at the point of contact? In other words, The first animation runs until it hits that x coordinate, and the rest of the animation takes place in the completion callback?
-- Should I use NSNotification observers and observe changes to the frame property? How accurate / granular is this? Can I track every change to x? Should I do this in a separate thread?
Any other suggestions would be welcome. I'm looking for a abest practice.
Use CADisplayLink since it is specifically built for this purpose. In the documentation, it says:
Once the display link is associated with a run loop, the selector on the target is called when the screen’s contents need to be updated.
For me I had a bar that fills up, and as it passed a certain mark, I had to change the colors of the view above that mark.
This is what I did:
let displayLink = CADisplayLink(target: self, selector: #selector(animationDidUpdate))
displayLink.frameInterval = 3
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode)
UIView.animateWithDuration(1.2, delay: 0.0, options: [.CurveEaseInOut], animations: {
self.viewGaugeGraph.frame.size.width = self.graphWidth
self.imageViewGraphCoin.center.x = self.graphWidth
}, completion: { (_) in
displayLink.invalidate()
})
func animationDidUpdate(displayLink: CADisplayLink) {
let presentationLayer = self.viewGaugeGraph.layer.presentationLayer() as! CALayer
let newWidth = presentationLayer.bounds.width
switch newWidth {
case 0 ..< width * 0.3:
break
case width * 0.3 ..< width * 0.6:
// Color first mark
break
case width * 0.6 ..< width * 0.9:
// Color second mark
break
case width * 0.9 ... width:
// Color third mark
break
default:
fatalError("Invalid value observed. \(newWidth) cannot be bigger than \(width).")
}
}
In the example, I set the frameInterval property to 3 since I didn't have to rigorously update. Default is 1 and it means it will fire for every frame, but it will take a toll on performance.
create a NSTimer with some delay and run particular selector after each time lapse. In that method check the frame of animating view and compare it with your colliding view.
And make sure you use presentationLayer frame because if you access view.frame while animating, it gives the destination frame which is constant through out the animation.
CGRect animationViewFrame= [[animationView.layer presentationLayer] frame];
If you don't want to create timer, write a selector which calls itself after some delay.Have delay around .01 seconds.
CLARIFICATION->
Lets say you have a view which you are animating its position from (0,0) to (100,100) with duration of 5secs. Assume you implemented KVO to the frame of this view
When you call the animateWithDuration block, then the position of the view changes directly to (100,100) which is final value even though the view moves with intermediate position values.
So, your KVO will be fired one time at the instant of start of animation.
Because, layers have layer Tree and Presentation Tree. While layer tree just stores destination values while presentation Layer stores intermediate values.
When you access view.frame it will always gives the value of frame in layer tree not the intermediate frames it takes.
So, you had to use presentation Layer frame to get intermediate frames.
Hope this helps.
UIDynamics and collision behaviours would be worth investigating here. You can set a delegate which is called when a collision occurs.
See the collision behaviour documentation for more details.

Observing change in frame of a UIView during animation

I want to observe changes to the x coordinate of my UIView's origin while it is being animated using animateWithDuration:delay:options:animations:completion:. I want to track changes in the x coordinate during this animation at a granular level because I want to make a change in interaction to another view that the view being animated may make contact with. I want to make that change at the exact point of contact. I want to understand the best way to do something like this at a higher level:
-- Should I use animateWithDuration:... in the completion call back at the point of contact? In other words, The first animation runs until it hits that x coordinate, and the rest of the animation takes place in the completion callback?
-- Should I use NSNotification observers and observe changes to the frame property? How accurate / granular is this? Can I track every change to x? Should I do this in a separate thread?
Any other suggestions would be welcome. I'm looking for a abest practice.
Use CADisplayLink since it is specifically built for this purpose. In the documentation, it says:
Once the display link is associated with a run loop, the selector on the target is called when the screen’s contents need to be updated.
For me I had a bar that fills up, and as it passed a certain mark, I had to change the colors of the view above that mark.
This is what I did:
let displayLink = CADisplayLink(target: self, selector: #selector(animationDidUpdate))
displayLink.frameInterval = 3
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode)
UIView.animateWithDuration(1.2, delay: 0.0, options: [.CurveEaseInOut], animations: {
self.viewGaugeGraph.frame.size.width = self.graphWidth
self.imageViewGraphCoin.center.x = self.graphWidth
}, completion: { (_) in
displayLink.invalidate()
})
func animationDidUpdate(displayLink: CADisplayLink) {
let presentationLayer = self.viewGaugeGraph.layer.presentationLayer() as! CALayer
let newWidth = presentationLayer.bounds.width
switch newWidth {
case 0 ..< width * 0.3:
break
case width * 0.3 ..< width * 0.6:
// Color first mark
break
case width * 0.6 ..< width * 0.9:
// Color second mark
break
case width * 0.9 ... width:
// Color third mark
break
default:
fatalError("Invalid value observed. \(newWidth) cannot be bigger than \(width).")
}
}
In the example, I set the frameInterval property to 3 since I didn't have to rigorously update. Default is 1 and it means it will fire for every frame, but it will take a toll on performance.
create a NSTimer with some delay and run particular selector after each time lapse. In that method check the frame of animating view and compare it with your colliding view.
And make sure you use presentationLayer frame because if you access view.frame while animating, it gives the destination frame which is constant through out the animation.
CGRect animationViewFrame= [[animationView.layer presentationLayer] frame];
If you don't want to create timer, write a selector which calls itself after some delay.Have delay around .01 seconds.
CLARIFICATION->
Lets say you have a view which you are animating its position from (0,0) to (100,100) with duration of 5secs. Assume you implemented KVO to the frame of this view
When you call the animateWithDuration block, then the position of the view changes directly to (100,100) which is final value even though the view moves with intermediate position values.
So, your KVO will be fired one time at the instant of start of animation.
Because, layers have layer Tree and Presentation Tree. While layer tree just stores destination values while presentation Layer stores intermediate values.
When you access view.frame it will always gives the value of frame in layer tree not the intermediate frames it takes.
So, you had to use presentation Layer frame to get intermediate frames.
Hope this helps.
UIDynamics and collision behaviours would be worth investigating here. You can set a delegate which is called when a collision occurs.
See the collision behaviour documentation for more details.

Get current CAAnimation transform value

I want to access get the value of the transform scale at a point in time. Here is the animation creation :
CABasicAnimation *grow = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
grow.toValue = #1.5f;
grow.duration = 1;
grow.autoreverses = YES;
grow.repeatCount = HUGE_VALF;
[view.layer addAnimation:grow forKey:#"growAnimation"];
I'd like to get, for example when a user presses a button, the current size of the view.
Logging the frame or the bounds always returns constant values. Any help would be much appreciated !
In Core Animation if an animation is "in flight" on a layer, the layer has a second layer property known as presentationLayer. That layer contains the values of the in-flight animation.
Edited
(With a nod to Mohammed, who took the time to provide an alternate answer for Swift)
Use code like this:
Objective-C
CGFloat currentScale = [[layer.presentationLayer valueForKeyPath: #"transform.scale"] floatValue];
Swift:
let currentScale = layer.presentation()?.value(forKeyPath: "transform.scale") ?? 0.0
Note that the Swift code above will return 0 if the layer does not have a presentation layer. That's a fail case that you should program for. It might be better to leave it an optional and check for a nil return.
Edit #2:
Note that as Kentzo pointed out in their comment, reading a value from an in-flight animation will give you a snapshot at an instant in time. The new value of the parameter you read (transform.scale in this example) will start to deviate from the reading you get. You should either pause the animation, or use the value you read quickly and get another value each time you need it.
The CALayer documentation describes presentationLayer quite clearly:
The layer object returned by this method provides a close approximation of the layer that is currently being displayed onscreen. While an animation is in progress, you can retrieve this object and use it to get the current values for those animations.
swift version of #Duncan C answer will be:
let currentValue = someView.layer.presentation()?.value(forKeyPath: "transform.scale")
Swift 5: The nicest answer, in a function from Hacking With Swift
func scale(from transform: CGAffineTransform) -> Double {
return sqrt(Double(transform.a * transform.a + transform.c * transform.c))
}

Jumpy CADisplayLink Animation

As detailed in a previous post (Here), I'm creating an animation that starts at an initial angle and moves to an ending angle. I've decided to use CADisplayLink for this animation because the animation needs to run as fast as possible during user input, so a CALayer with a CAKeyframeAnimation seemed like it would be too slow to achieve this.
After implementing the CADisplayLink, which calls setNeedsDisplay, I do have the animation working, but it looks really bad because it chunks up the difference between endAngle and initialAngle into heavily visible blocks of angles instead of creating a continuous flow from one angle to the next. Here's the current code I have:
CGFloat newAngleToAnimate = animationProgress + ((endAngle-initialAngle)/kDrawDuration)*elapsedTime;
// Use newAngleToAnimate to draw in drawInContext
animationProgress = newAngleToAnimate; // Update the progress for the next frame.
Also, kDrawDuration is defined as 3.0f, so I want the animation from initialAngle to endAngle to take 3.0 seconds. I break up the full circle (2*M_PI radians) into equal segments by calculating 2*M_PI / kNumAnglesInAnimation, and preferably I want to animate one of those angles every frame, but somehow I still have to take kDrawDuration and elapsedTime into account, which I'm just not seeing how to achieve.
Thanks for any help with fixing this!
CGFloat newAngleToAnimate = animationProgress + ((endAngle-initialAngle)/kDrawDuration)*elapsedTime;
don't track "animationProgress". Your elapsedTime is all you need in order to get your animation correct. So just remove it and use:
CGFloat newAngleToAnimate = ((endAngle-initialAngle)/kDrawDuration)*elapsedTime;

Resources