Understanding animateKeyframes relative start times/delays - ios

I'm either being stupid, or misunderstanding how keyframe animations work on iOS (or both!). The two animation blocks below produce different results but I would expect them to be the same:
let duration: TimeInterval = 2
UIView.animateKeyframes(withDuration: duration, delay: 0, animations: {
UIView.addKeyframe(withRelativeStartTime: 0.9, relativeDuration: 0.1, animations: {
self.someView.transform = CGAffineTransform(translationX: 0, y: 150)
})
})
UIView.animateKeyframes(withDuration: duration * 0.1, delay: duration * 0.9, animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1, animations: {
self.someView.transform = CGAffineTransform(translationX: 0, y: 150)
})
})
Can anyone help me understand why these are different when executed? The first seems to do what I expect, but the second one seems to execute the animation earlier than expected.

You're seeing the effects of the default timing curve on your keyframe animations. The first animation is the last 0.2 seconds of a 2 second animation, and the second animation is the entirety of a 0.2 second animation. The default ease-in-ease-out means that the first animation will be done entirely in the ease-out portion of the curve.
To force a linear curve on both animations you can wrap them inside another animation and set a couple of options:
UIView.animate(withDuration: duration, delay: 0, options: [.curveLinear], animations: {
UIView.animateKeyframes(withDuration: duration, delay: 0, options: [.overrideInheritedDuration, .calculationModeLinear], animations: {
UIView.addKeyframe(withRelativeStartTime: 0.9, relativeDuration: 0.1, animations: {
view1.transform = CGAffineTransform(translationX: 0, y: 150)
})
})
UIView.animateKeyframes(withDuration: duration * 0.1, delay: duration * 0.9, options: [.overrideInheritedDuration, .calculationModeLinear], animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1, animations: {
view2.transform = CGAffineTransform(translationX: 0, y: 150)
})
})
}, completion: nil)
Which I think you'll agree is horrible-looking code, but I'm assuming this is an intellectual exercise :)

Related

Second keyframe in UIView.animateKeyframes not getting called

Currently there are 2 CGAffineTransform key frames added to the UIView.animateKeyframes. For some strange reason the second keyframe's animation doesn't appear to happen. It just jumps right back to the starting position after the animation completes.
UIView.animateKeyframes(withDuration: 3, delay: 0, options: [], animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 1, animations: {
let scaledBy = CGAffineTransform(scaleX: 1.0, y: 1.0)
self.firstView.transform = scaledBy.translatedBy(x: 120, y: 20).rotated(by: CGFloat.pi/2)
})
UIView.addKeyframe(withRelativeStartTime: 1, relativeDuration: 1, animations: {
let scaledBy = CGAffineTransform(scaleX: 1.5, y: 1.5)
self.firstView.transform = scaledBy.translatedBy(x: 40, y: 60).rotated(by: -CGFloat.pi/2)
self.firstView.transform = CGAffineTransform(rotationAngle: -CGFloat.pi/2)
})
}, completion: {_ in})
It seems like this is very old question. I hope my answer will be helpful though. The issue here is because you didn't get the meaning of relativeStartTime and relativeDuration parameters. The key word here is relative. It means that all your inner animations, added via UIView.addKeyframe function should happen between 0 and 1.
Because this call will create a kind of timeline
UIView.animateKeyframes(withDuration: 3, delay: 0, options: [], animations: {
}, completion: {_ in})
This timeline runs from 0 to 1 (0% to 100%). Which means that relative duration of any animation can't be more than 1 and relative start time also can't be more than 1.

UIView.animateKeyframes jumps to start of next animation instead of transitioning smoothly

I'm trying to create an animation where an object moves from one keypoint to the next smoothly. As of right now the object does move to the different key points but instead of transitioning to each one as soon as it finishes one animation it automatically jumps to the next keypoint but with no transition animation in between. This is the function I'm using:
func keyframeAnimate () {
UIView.animateKeyframes(withDuration: 3, delay: 0, options: [], animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1.0, animations: {
let translation = CGAffineTransform(translationX: 150, y: 150)
self.square.alpha = 0.5
self.square.transform = translation.rotated(by: CGFloat.pi).scaledBy(x: 2, y: 2)
})
UIView.addKeyframe(withRelativeStartTime: 1.01, relativeDuration: 1.0, animations: {
let translation = CGAffineTransform(translationX: 100, y: 400)
self.square.alpha = 1
self.square.transform = translation.rotated(by: -CGFloat.pi).scaledBy(x: 1, y: 1)
})
})
}
Any help is appreciated!
Your problem is in the relative start and relative duration. As the documentation explains, these parameters should be values between 0 and 1 because these are fractions of the entire animateKeyframes(withDuration: duration

How to time UIImpactFeedbackGenerator with a UIView Key Frame Animation?

I'm trying to add impact feedback to a custom view controller presentation. The animation is implemented in key frames, and has a bounce at the end so I can't perform the impact in presentationTransitionDidEnd. My key frames look something like:
UIView.animateKeyframes(withDuration: 0.6, delay: 0, options: [], animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5, animations: {
// View controller hits its full size; I'd like feedback at the end of this
}
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.25, animations: {
// Expand for bounce
}
UIView.addKeyframe(withRelativeStartTime: 0.75, relativeDuration: 0.25, animations: {
// Back to normal size
}
}, completion: { ... })
Is there a way to run a block of code at a specific point in a key frame animation? Placing it right inside the block doesn't work.

Adding easing to animateKeyframes

I'm animating the scale of a button using animateKeyframes in Swift 3 using the following code:
UIView.animateKeyframes(withDuration: 4, delay: 1, options: [.repeat, .allowUserInteraction, .calculationModeCubic], animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.1, animations: {
self.myButton.transform = CGAffineTransform(scaleX: 1,y: 1)
})
UIView.addKeyframe(withRelativeStartTime: 0.1, relativeDuration: 0.1, animations: {
self.myButton.transform = CGAffineTransform(scaleX: 0.9,y: 0.9)
})
}, completion: nil)
What I need to do is add easing to each keyframe. I looked up something that uses the following code that can then be passed to animateKeyframes
let animationOptions: UIViewAnimationOptions = .curveEaseIn
let keyframeAnimationOptions: UIViewKeyframeAnimationOptions = UIViewKeyframeAnimationOptions(rawValue: animationOptions.rawValue)
However, it doesn't quite say exactly how to pass the keyframeAnimationOptions variable, and I can't seem to figure it out myself! Can anybody help?
You can use below code to call the keyframeAnimationOptions variable.
let keyframeAnimationOption=UIViewKeyframeAnimationOptions.CalculationModeCubic
UIView.animateKeyframesWithDuration(duration, delay: delay, options: keyframeAnimationOption, animations:
{
/////Your code //////
}, completion: nil)

Multiple UIView Animations

I have a UIButton that I have placed in the center of the screen using Interface Builder.
I want to animate the UIButton to move up and down and repeat the animation forever.
So far I have this in my didMoveToView:
UIView.animateWithDuration(3, animations: { () -> Void in
self.playBtn.transform = CGAffineTransformMakeTranslation(0, 10)
self.playBtn.transform = CGAffineTransformMakeTranslation(0, -10)
self.playBtn.transform = CGAffineTransformMakeTranslation(0, -10)
self.playBtn.transform = CGAffineTransformMakeTranslation(0, 10)
UIView.setAnimationRepeatCount(-1)
})
However it only runs the first line and moves the button down 10.
You can't change the transform to multiple things simultaneously. I would expect your code to cause no animation at all, as changing the transform more than once would cancel the animation.
Another problem with your code is that what you're saying is not how you ask for a repeating animation.
Still another problem is that you won't ever bring the button back to its starting place; the transforms are not additive.
What you need to do is chain animations together. This will be easiest if you drop down to Core Animation and make a grouped animation.
Or can do it with view animation by doing a keyframe animation, perhaps; this is not identical to what you want, but it will get you started:
Swift 2
let opts = UIViewKeyframeAnimationOptions.Repeat
UIView.animateKeyframesWithDuration(3, delay: 0, options: opts, animations: {
UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0.5, animations: {
self.playBtn.transform = CGAffineTransformMakeTranslation(0, 10)
})
UIView.addKeyframeWithRelativeStartTime(0.5, relativeDuration: 0.5, animations: {
self.playBtn.transform = CGAffineTransformIdentity
})
}, completion: nil)
Swift 3,4,5
let opts = UIView.KeyframeAnimationOptions.repeat
UIView.animateKeyframes(withDuration: 3, delay: 0, options: opts, animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5, animations: {
self.playBtn.transform = CGAffineTransform(translationX: 0, y: 10)
})
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5, animations: {
self.playBtn.transform = CGAffineTransform.identity
})
}, completion: nil)
But really, it would be better if you learned how animation actually works before you get into this kind of thing. From your code, it appears to me you are just thrashing.

Resources