UIViewPropertyAnimator with short animation segments in the beginning - ios

Imagine UIViewPropertyAnimator is setup as follows:
let animator = UIViewPropertyAnimator(duration: 2, curve: .easeInOut) {
myView.center = newCenterPoint
}
For this, I can add another animation that starts with a delay, ends with the main:
animator.addAnimation ({
myView.alpha = 0.5
}, withDelayFactor: 0.5)
So above starts at 1 second and continues for 1 second to finish with the main.
Question
Is there a way to add an animation to UIViewPropertyAnimator that starts at the beginning, but continues for a fraction of the time of the main animator? Starts at 0 and continues only for 1 second?

Why not just use another animator? It's fine for two animators to act on the same view simultaneously as long as their animations don't conflict:
let animator = UIViewPropertyAnimator(duration: 2, curve: .easeInOut) {
self.myView.center.y += 100
}
let animator2 = UIViewPropertyAnimator(duration: 1, curve: .easeInOut) {
self.myView.backgroundColor = .red
}
animator.startAnimation(); animator2.startAnimation()
The animation moves the view downward for two seconds, while turning the view red during the first one second.
(Having said all that, in real life I'd probably use a CAAnimationGroup for this, rather than a property animator.)

Related

How to add a pause/wait in the middle of a nested SCNTransaction chain

This question must have a very simple answer, but I couldn't find it after searching for hours. Unless I just need to manually add some timer to wait.
Basically, I have an animation sequence to move a node to a certain place, here I want to wait 5 seconds and then I have another animation sequence to bring the node back to its original position.
Initially, I used SCNActions, grouping some actions and then sequencing them all. Here I just added an SCNAction.wait(duration: 5) and that did the trick.
However, one of the actions is to move the node 90 degrees around the X-axis and I need to simultaneously rotate around another axis as well. The result is incorrect and I have a feeling I've run into the gimbal lock issue.
So instead of using SCNAction.rotate(by: ) I decided to rotate using quaternions which don't have the gimbal lock problem, but then I needed to switch to using an SCNTransaction.
I'm nesting these transaction in the completionBlock of the previous SCNTransaction.
For the pause between the 2 animations, I don't have a wait action here, but I thought that just adding a transaction with a duration that does nothing will at least wait for as long. But it doesn't. The transaction immediately skips to the 3rd step.
So is there a simple command to tell the animation transaction to wait?
I did try to add DispatchQueue.main.asyncAfter(deadline: .now() + 5) { <3rd SCNTransaction here> } and that worked, but is that really the way to do it? I have a feeling there's something else that I just don't know that replaces SCNAction.wait().
Here's my current code which does the animations correctly, except it doesn't wait where I want it to (this is an excerpt of the relevant code only):
[...]
let moveSideQuat = simd_quatf(angle: -currentYRotation, axis: simd_float3(0, 0, 1))
let moveAwaySimd = simd_float3(-20 * sinf(currentYRotation), 0, -20 * cosf(currentYRotation))
let moveUpQuat = simd_quatf(angle: .pi/2, axis: simd_float3(1, 0, 0))
let moveDownQuat = simd_quatf(angle: -.pi/2, axis: simd_float3(1, 0, 0))
let moveTowardsQuat = simd_float3(0, 0, pivotZLocation)
let moveBackSideQuat = simd_quatf(angle: currentYRotation, axis: simd_float3(0, 0, 1))
SCNTransaction.begin()
SCNTransaction.animationDuration = 1
baseNode?.simdOrientation = moveSideQuat * baseNode!.simdOrientation
baseNode?.simdPosition = moveAwaySimd
baseNode?.simdOrientation = moveUpQuat * baseNode!.simdOrientation
SCNTransaction.completionBlock = {
SCNTransaction.begin()
SCNTransaction.animationDuration = 5 // <<<--- I WANT TO WAIT HERE
SCNTransaction.completionBlock = {
SCNTransaction.begin()
SCNTransaction.animationDuration = 1
self.baseNode?.simdOrientation = moveDownQuat * self.baseNode!.simdOrientation
self.baseNode?.simdPosition = moveTowardsQuat
self.baseNode?.simdOrientation = moveBackSideQuat * self.baseNode!.simdOrientation
SCNTransaction.commit()
}
SCNTransaction.commit()
}
SCNTransaction.commit()
[...]
In the 2nd completion block, after the animationDuration = 5 I even tried to add a simple assignment for the node's simdPosition, basically leaving it in the same position in the hope to trick the sequence to take 5 seconds to basically do nothing, but it still skipped to the next completionBlock.
So is the asyncAfter actually the way to go or am I missing something else?
Thanks!
Using asyncAfter to wait is fine. That is the purpose of this method, after all.
Creating an empty transaction with the duration of 5 seconds doesn't work because SceneKit can easily see that you haven't changed anything, and knows that the animation can complete immediately. You can hide a node somewhere in the scene and animate that node for 5 seconds, but that feels way more like a hack.
If you want, you can still use SCNActions for the rest of your animations, and just use SCNTransaction for the ones that need the quaternions. For example:
let step1 = SCNAction.group([
SCNAction.run {
SCNTransaction.begin()
SCNTransaction.animationDuration = 1
// do rotations here with quaternions...
SCNTransaction.commit()
},
SCNAction.move(to: /* destination */, duration: 1)
])
let step2 = SCNAction.wait(duration: 5)
let step3 = SCNAction.group([
SCNAction.run {
SCNTransaction.begin()
SCNTransaction.animationDuration = 1
// rotate back...
SCNTransaction.commit()
},
SCNAction.move(to: /* origin */, duration: 1)
])
let entireAction = SCNAction.sequence([
step1, step2, step3
])

How to properly set the CABasicAnimation (begin) time?

I animate the color of CAShapeLayers stored in an Array using CABasicAnimation. The animation displays erratically depending on the animation.duration and I cannot figure out why. I suspect an issue with animation.beginTime = CACurrentMediaTime() + delay
Animation Description
The animation consists in successively flashing shapes to yellow before turning them to black once the animation ends.
Current State of the animation
When the animation duration is above a certain time, it works properly.
For instance with a duration of 2 seconds:
But when I shorten the duration, the result substantially differs.
For instance, with a duration of 1 second:
You will notice that the animation has already cached/ended for the first 10 bars or so, then waits and starts animating the remainder of the shapes.
Likewise, with a duration of 0.5s:
In this case, it seems an even larger number of animation has already ended (shapes are black) before it displays some animation after a certain time. You can also notice that although the shape color animation is supposed to last the same duration (0.5s) some feels quicker than others.
The Code
The animation is called in the viewDidAppear method of the UIViewController class.
I have created a UIView custom class to draw my shapes and I animate them using an extension of the class.
The code to animate the color:
enum ColorAnimation{
case continuousSwap
case continousWithNewColor(color: UIColor)
case randomSwap
case randomWithNewColor(color: UIColor)
case randomFromUsedColors
}
func animateColors(for duration: Double,_ animationType: ColorAnimation, colorChangeDuration swapColorDuration: Double){
guard abs(swapColorDuration) != Double.infinity else {
print("Error in defining the shape color change duration")
return
}
let animDuration = abs(duration)
let swapDuration = abs(swapColorDuration)
let numberOfSwaps = Int(animDuration / min(swapDuration, animDuration))
switch animationType {
case .continousWithNewColor(color: let value):
var fullAnimation = [CABasicAnimation]()
for i in (0...numberOfSwaps) {
let index = i % (self.pLayers.count)
let fromValue = pLayers[index].pattern.color
let delay = Double(i) * swapDuration / 3
let anim = colorAnimation(for: swapDuration, fromColor: value, toColor: fromValue, startAfter: delay)
fullAnimation.append(anim)
}
for i in (0...numberOfSwaps) {
CATransaction.begin()
let index = i % (self.pLayers.count)
CATransaction.setCompletionBlock {
self.pLayers[index].shapeLayer.fillColor = UIColor.black.cgColor
}
pLayers[index].shapeLayer.add(fullAnimation[i], forKey: "fillColorShape")
CATransaction.commit()
}
default:
()
}
}
The segment the whole duration of the animation by the duration of the color change (e.g. if the whole animation is 10s and each shape changes color in 1s, it means 10 shapes will change color).
I then create the CABasicaAnimation objects using the method colorAnimation(for: fromColor, toColor, startAfter:).
func colorAnimation(for duration: TimeInterval, fromColor: UIColor, toColor: UIColor, reverse: Bool = false, startAfter delay: TimeInterval) -> CABasicAnimation {
let anim = CABasicAnimation(keyPath: "fillColor")
anim.fromValue = fromColor.cgColor
anim.toValue = toColor.cgColor
anim.duration = duration
anim.autoreverses = reverse
anim.beginTime = CACurrentMediaTime() + delay
return anim
}
Finally I add the animation to the adequate CAShapeLayer.
The code can obviously be optimized but I chose to proceed by these steps to try to find why it was not working properly.
Attempts so far
So far, I have tried:
with and without setting the animation.beginTime in the colorAnimation method, including with and without CACurrentMediaTime(): if I don't set the animation.beginTime with CACurrentMediaTime, I simply do not see any animation.
with and without pointing animation.delegate = self: it did not change anything.
using DispatchQueue (store the animations in global and run it in main) and as suspected, the shapes did not animate.
I suspect something is not working properly with the beginTime but it might not be the case, or only this because even when the shapes animate, the shape animation duration seems to vary whilst it should not.
Thank very much in advance to have a look to this issue. Any thoughts are welcome even if it seems far-fetched it can open to new ways to address this!
Best,
Actually there is a relationship between duration and swapColorDuration
func animateColors(for duration: Double,_ animationType: ColorAnimation, colorChangeDuration swapColorDuration: Double)
when you call it, you may need to keep this relationship
let colorChangeDuration: TimeInterval = 0.5
animateColors(for: colorChangeDuration * TimeInterval(pLayers.count), .continousWithNewColor(color: UIColor.black), colorChangeDuration: colorChangeDuration)
Also here :
let numberOfSwaps = Int(animDuration / min(swapDuration, animDuration)) - 1
This value maybe a little higher than you need.
or
The problem lies in this let index = i % (self.pLayers.count)
if numberOfSwaps > self.pLayers.count, some bands will be double animations.
let numberOfSwaps1 = Int(animDuration / min(swapDuration, animDuration))
let numberOfSwaps = min(numberOfSwaps1, self.pLayers.count)
in the rest is
for i in (0..<numberOfSwaps) {... }
Now if numberOfSwaps < self.pLayers.count. It's not finished.
if numberOfSwaps is larger, It is fine.
If double animations are required, changes the following:
pLayers[index].shapeLayer.add(fullAnimation[i], forKey: nil)
or pLayers[index].shapeLayer.add(fullAnimation[i], forKey: "fillColorShape" + String(i))

Animating Views and subviews in sequence

Im struck with Animation. I would like to animate in below sequence as shown in picture.
Please click here for Image
All are views i.e., outerView, dot1, dot2, dot3 . I've implemented code to animate dots but need your help to animate outerview and adding everything in sequence
let transition = CATransition()
transition.duration = 2;
transition.type = kCATransitionPush;
transition.subtype = kCATransitionFromLeft;
transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionDefault)
transition.speed = 1.0
dot3?.layer.add(transition, forKey: nil)
transition.beginTime = CACurrentMediaTime() + 0.11
dot2?.layer.add(transition, forKey: nil)
transition.beginTime = CACurrentMediaTime() + 0.22
dot1?.layer.add(transition, forKey: nil)
Please help me animating in sequence - outerView starting, dots and closing outerView like shown
You're on the right path, except obviously there will be a lot more animations than the few that you've shown in the snippet. There's no reason why you can't continue building this animation using the CAAnimation classes, but I suspect that using the newer UIViewPropertyAnimator classes (will need to target iOS10) will be useful because they allow you to 'scrub' the steps in the animation which will be useful debugging. Here's a good intro: dzone.com/articles/ios-10-day-by-day-uiviewpropertyanimator
Expanding on this comment to a proper answer...
Using animateWithKeyframes is a pretty decent solution to create this animation in code. Here's a snippet of what this could look like:
let capsule: UIView // ... the container view
let capsuleDots [UIView] //... the three dots
let capsuleFrameWide, capsuleFrameNarrow: CGRect //.. frames for the capsule
let offstageLeft, offstageRight: CGAffineTransform // transforms to move dots to left or right
let animator = UIViewPropertyAnimator(duration: 2, curve: .easeIn)
// the actual animation occurs in 4 steps
animator.addAnimations {
UIView.animateKeyframes(withDuration: 2, delay: 0, options: [.calculationModeLinear], animations: {
// step 1: make the capsule grow to large size
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.1) {
capsule.bounds = capsuleFrameWide
}
// step 2: move the dots to their default positions, and fade in
UIView.addKeyframe(withRelativeStartTime: 0.1, relativeDuration: 0.1) {
capsuleDots.forEach({ dot in
dot.transform = .identity
dot.alpha = 1.0
})
}
// step 3: fade out dots and translate to the right
UIView.addKeyframe(withRelativeStartTime: 0.8, relativeDuration: 0.1) {
capsuleDots.forEach({ dot in
dot.alpha = 0.0
dot.transform = offstageRight
})
}
// step4: make capsure move to narrow width
UIView.addKeyframe(withRelativeStartTime: 0.9, relativeDuration: 0.1) {
capsule.bounds = capsuleFrameNarrow
}
})
}
Wrapping the keyframes in a UIViewPropertyAnimator makes it easy to scrub the animation (among other things).
In case it's useful for anyone, I've pushed a small project to GitHub that allows you to jump in an explore/refine/debug animations with UIViewPropertyAnimator. It includes boilerplate for connecting the UISlider to the animation so all you have to focus on is the animation itself.
This is all for debugging the animation, for production of course you'll probably want to remove hard coded sizes so it can be potentially reused at different scales etc.
It very easy to implement
animatedImage(with:duration:)
or
var animationImages: [UIImage]?
example:
UIImageView.animationImages = [image1, image2, image3, image4,...]
UIImageView.animationDuration = 5
UIImageView.startAnimating()
You will get ordered animation with couple of lines only

Delay after each animation iteration in Swift

I have a Core Animation whose .repeatCount is set to Float.infinity. After each iteration of the Animation, ie. after each repetition, I want to have a delay of 3 seconds. How can I achieve this? Thanks!
You can use a function like the following to do what you need.
func animateInfinitelyWithDelay(delay: TimeInterval, duration: TimeInterval) {
UIView.animate(
withDuration: duration,
delay: delay,
options: UIView.AnimationOptions.curveEaseIn,
animations: { () -> Void in
// Your animation Code
}) { (finished) -> Void in
if finished {
self.animateInfinitelyWithDelay(delay: delay, duration: duration)
}
}
}
One way to accomplish this effect using Core Animation is to configure everything except the repeat count on the original animation object and then wrap it in an animation group with a longer duration and repeat that animation group instead.
let originalAnimation = /* create and configure original animation ... */
originalAnimation.duration = shortDuration
let group = CAAnimationGroup()
group.animations = [originalAnimation]
group.duration = shortDuration + delayAtTheEnd
group.repeatCount = .infinity
theLayer.add(group, forKey: "repeating animation with delay between iterations")
Note that depending on what the original animation is doing, you may need to configure a fill mode to achieve the right look.
You could also use a UIView keyframe animation (animateKeyframesWithDuration) where there is "dead time" built into the animation at the end, and then repeat that animation.

Staggered animations with CAKeyframeAnimation?

I want to animate 3 different images at specific point in time such that it behaves this way.
1) 1st image moves from (Xx, Yx) to (Xz,Yz)
2) Wait 10 seconds
3) 2nd image appears in place at Xa,Yb
4) Wait half as long as in step 2
5) Fade out 2nd image
6) 3rd image appears at the same place as 2nd image
If each of these image's animations are on their own CALayers, can I use CAKeyframeAnimation with multiple layers? If not, what's another way to go about doing staggered animations?
I'm trying to animate a playing card move from offscreen to a particular spot and then few other tricks to appear on screen several seconds later.
Edited
When I wrote this, I thought you could not use a CAAnimationGroup to animate multiple layers. Matt just posted an answer demonstrating that you can do that. I hereby eat my words.
I've taking the code in Matt's answer and adapted it to a project which I've uploaded to Github (link.)
The effect Matt's animation creates is of a pair of feet walking up the screen. I found some open source feet and installed them in the project, and made some changes, but the basic approach is Matt's. Props to him.
Here is what the effect looks like:
(The statement below is incorrect)
No, you can't use a keyframe animation to animate multiple layers. A given CAAnimation can only act on a single layer. This includes group layers, by the way.
If all you're doing is things like moving images on a straight line, fading out, and fading in, why don't you use UIView animation? Take a look at the methods who's names start with animateWithDuration:animations: Those will let you create multiple animations at the same time, and the completion block can then trigger additional animations.
If you need to use layer animation for some reason, you can use the beginTime property (which CAAnimation objects have because they conform to the CAMediaTiming protocol.) For CAAnimations that are not part of an animation group, you use
animation.beginTime = CACurrentMediaTime() + delay;
Where delay is a double which expresses the delay in seconds.
If the delay is 0, the animation would begin.
A third option would be to set your view controller up as the delegate of the animation and use the animationDidStop:finished: method to chain your animations. This ends up being the messiest approach to implement, in my opinion.
The claim that a single animation group cannot animate properties of different layers is not true. It can. The technique is to attach the animation group to the superlayer and refer to the properties of the sublayers in the individual animations' key paths.
Here is a complete example just for demonstration purposes. When launched, this project displays two "footprints" that proceed to step in alternation, walking off the top of the screen.
class ViewController: UIViewController, CAAnimationDelegate {
let leftfoot = CALayer()
let rightfoot = CALayer()
override func viewDidLoad() {
super.viewDidLoad()
self.leftfoot.name = "left"
self.leftfoot.contents = UIImage(named:"leftfoot")!.cgImage
self.leftfoot.frame = CGRect(x: 100, y: 300, width: 50, height: 80)
self.view.layer.addSublayer(self.leftfoot)
self.rightfoot.name = "right"
self.rightfoot.contents = UIImage(named:"rightfoot")!.cgImage
self.rightfoot.frame = CGRect(x: 170, y: 300, width: 50, height: 80)
self.view.layer.addSublayer(self.rightfoot)
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.start()
}
}
func start() {
let firstLeftStep = CABasicAnimation(keyPath: "sublayers.left.position.y")
firstLeftStep.byValue = -80
firstLeftStep.duration = 1
firstLeftStep.fillMode = .forwards
func rightStepAfter(_ t: Double) -> CABasicAnimation {
let rightStep = CABasicAnimation(keyPath: "sublayers.right.position.y")
rightStep.byValue = -160
rightStep.beginTime = t
rightStep.duration = 2
rightStep.fillMode = .forwards
return rightStep
}
func leftStepAfter(_ t: Double) -> CABasicAnimation {
let leftStep = CABasicAnimation(keyPath: "sublayers.left.position.y")
leftStep.byValue = -160
leftStep.beginTime = t
leftStep.duration = 2
leftStep.fillMode = .forwards
return leftStep
}
let group = CAAnimationGroup()
group.duration = 11
group.animations = [firstLeftStep]
for i in stride(from: 1, through: 9, by: 4) {
group.animations?.append(rightStepAfter(Double(i)))
group.animations?.append(leftStepAfter(Double(i+2)))
}
group.delegate = self
self.view.layer.add(group, forKey: nil)
}
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
print("done")
self.rightfoot.removeFromSuperlayer()
self.leftfoot.removeFromSuperlayer()
}
}
Having said all that, I should add that if you are animating a core property like the position of something, it might be simpler to make it a view and use a UIView keyframe animation to coordinate animations on different views. Still, the point is that to say that this cannot be done with CAAnimationGroup is just wrong.

Resources