When doing UIView.animateWithDuration, I would like to define a custom curve for the ease, as opposed to the default: .CurveEaseInOut, .CurveEaseIn, .CurveEaseOut, .CurveLinear.
This is an example ease that I want applied to UIView.animateWithDuration:
let ease = CAMediaTimingFunction(controlPoints: Float(0.8), Float(0.0), Float(0.2), Float(1.0))
I tried making my own UIViewAnimationCurve, but it seems it accepts only one Int.
I can apply the custom ease to a Core Animation, but I would like to have custom easing for UIView.animateWithDuration for simpler and optimized code. UIView.animateWithDuration is better for me as I won't have to define animations for each animated property and easier completion handlers, and to have all animation code in one function.
Here's my non-working code so far:
let customOptions = UIViewAnimationOptions(UInt((0 as NSNumber).integerValue << 50))
UIView.setAnimationCurve(UIViewAnimationCurve(rawValue: 5)!)
UIView.animateWithDuration(2, delay: 0, options: customOptions, animations: {
view.layer.position = toPosition
view.layer.bounds.size = toSize
}, completion: { finished in
println("completion")
})
That's because UIViewAnimationCurve is an enumeration - its basically human-readable representations of integer values used to determine what curve to use.
If you want to define your own curve, you need to use CA animations.
You can still do completion blocks and groups of animations. You can group multiple CA Animations into a CAAnimationGroup
let theAnimations = CAAnimationGroup()
theAnimations.animations = [positionAnimation, someOtherAnimation]
For completion, use a CATransaction.
CATransaction.begin()
CATransaction.setCompletionBlock { () -> Void in
// something?
}
// your animations go here
CATransaction.commit()
After a bunch of researches, the following code works for me.
Create an explicit core animation transaction, set your desired timing function.
Use eigher or both UIKit and CAAnimation to perform the changes.
let duration = 2.0
CATransaction.begin()
CATransaction.setAnimationDuration(duration)
CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(controlPoints: 0.8, 0.0, 0.2, 1.0))
CATransaction.setCompletionBlock {
print("animation finished")
}
// View animation
UIView.animate(withDuration: duration) {
// E.g. view.center = toPosition
}
// Layer animation
view.layer.position = toPosition
view.layer.bounds.size = toSize
CATransaction.commit()
With iOS 10.0 you can use UIViewPropertyAnimator.
https://developer.apple.com/documentation/uikit/uiviewpropertyanimator
See https://developer.apple.com/videos/play/wwdc2016/216/
Related
In Swift, I would like to have several calls to UIView.animate run in series. That is, when one animation finishes, then I would like another animation to continue after, and so on.
The call to UIView.animate has a Trailing Closure which I am currently using to make a second call to UIView.animate to occur.
The Problem is: I want to do N separate animations
From the Apple Documentation for UIView.animate
completion
A block object to be executed when the animation sequence ends. This block has no return value and takes a single Boolean argument that indicates whether or not the animations actually finished before the completion handler was called. If the duration of the animation is 0, this block is performed at the beginning of the next run loop cycle. This parameter may be NULL.
Ideally, I would like to iterate over an array of animation durations and use in the calls to animate()
For example,
Goal
Iterate over an array and apply those parameters for each animation
let duration = [3.0, 5.0, 10.0]
let alpha = [0.1, 0.5, 0.66]
Xcode Version 11.4.1 (11E503a)
What I've tried
Use a map to iterate, and 🤞 hope that it works
Issue is that that only final animation occurs. So there is nothing in series
Research Q: Is it possible that I need to set a boolean in UIView that says animation occur in series?
let redBox = UIView()
redBox.backgroundColor = UIColor.red
self.view.addSubview(redBox)
let iterateToAnimate = duration.enumerated().map { (index, element) -> Double in
print(index, element, duration[index])
UIView.animate(withDuration: duration[index], // set duration from the array
animations: { () in
redBox.alpha = alpha[index]
}, completion:{(Bool) in
print("red box has faded out")
})
}
How to do one animation
let redBox = UIView()
redBox.backgroundColor = UIColor.red
self.view.addSubview(redBox)
// One iteration of Animation
UIView.animate(withDuration: 1,
animations: { () in
redBox.alpha = 0
}, completion:{(Bool) in
print("red box has faded out")
})
Ugly way to do chain two animate iterations (wish to avoid)
// two iterations of Animation, using a trailing closure
UIView.animate(withDuration: 1,
animations: { () in
redBox.alpha = 0
}, completion:{(Bool) in
print("red box has faded out")
}) { _ in // after first animation finishes, call another in a trailing closure
UIView.animate(withDuration: 1,
animations: { () in
redBox.alpha = 0.75
}, completion:{(Bool) in
print("red box has faded out")
}) // 2nd animation
}
If the parameters are few and are known at compile time, the simplest way to construct chained animations is as the frames of a keyframe animation.
If the parameters are not known at compile time (e.g. your durations are going to arrive at runtime) or there are many of them and writing out the keyframe animation is too much trouble, then just put a single animation and its completion handler into a function and recurse. Simple example (adapt to your own purposes):
var duration = [3.0, 5.0, 10.0]
var alpha = [0.1, 0.5, 0.66] as [CGFloat]
func doTheAnimation() {
if duration.count > 0 {
let dur = duration.removeFirst()
let alp = alpha.removeFirst()
UIView.animate(withDuration: dur, animations: {
self.yellowView.alpha = alp
}, completion: {_ in self.doTheAnimation()})
}
}
you can use UIView.animateKeyframes
UIView.animateKeyframes(withDuration: 18.0, delay: 0.0, options: [], animations: {
UIView.addKeyframe(withRelativeStartTime: 3.0/15.0, relativeDuration: 3.0/15.0, animations: {
self.redBox.alpha = 0
})
UIView.addKeyframe(withRelativeStartTime: 8.0/15.00, relativeDuration: 5.0/15.0, animations: {
self.redBox.alpha = 0.75
})
}) { (completed) in
print("completed")
}
I've taken a look at the answers in the following StackOverflow question but none seem to work for me: on the execution of the completion block, instead of performing the animation again, the program spews out "complete" ad infinitum without animating the view at all.
How can I repeat animation (using UIViewPropertyAnimator) certain number of times?
This is my AnimatorFactory class:
class AnimatorFactory {
#discardableResult
static func rotateRepeat(view: UIView) -> UIViewPropertyAnimator {
let rotate = UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 1.0, delay: 0.0, options: [.curveLinear], animations: {
view.transform = CGAffineTransform(rotationAngle: .pi)
}, completion: { _ in
print("complete")
self.rotateRepeat(view: view)
})
return rotate
}
}
It is called as you'd expect with AnimatorFactory.rotateRepeat(view: <someView>)
However, the problem as mentioned above occurs. What I'd expect is that the view would rotate repeatedly until some time that I decide to change or stop it; this is exactly the reason that I have chosen to use UIViewPropertyAnimator instead of UIView.animate(withDuration:animations).
What's the best way then to create interactive, repeatable UIView animations? Much appreciated.
Your code is working fine. The trouble is that your animation does nothing after the first time. You say:
view.transform = CGAffineTransform(rotationAngle: .pi)
The first time, we change the rotation from 0 to pi. That is a change, so there is animation. But after that we just keep saying “stay at pi” over and over. We are at pi and you say to stay there, so there is no change to animate.
What you want each animation to do is add pi, not be pi.
As #matt suggested, I was merely setting the rotation of the view to .pi over and over. So in the completion block I have now set the transform to .identity before kicking the animation off again.
class AnimatorFactory {
#discardableResult
static func rotateRepeat(view: UIView) -> UIViewPropertyAnimator {
let rotate = UIViewPropertyAnimator(duration: 1.0, curve: .linear)
rotate.addAnimations {
view.transform = CGAffineTransform(rotationAngle: .pi)
}
rotate.addCompletion{ _ in
view.transform = .identity
self.rotateRepeat(view: view)
}
rotate.startAnimation()
return rotate
}
}
I am trying to remove my google map marker with fade out animation. And I have tried
CATransaction.begin()
CATransaction.setAnimationDuration(1.0)
myMarker.marker?.map = nil
CATransaction.commit()
CATransaction worked for myMarker.marker?.rotation but not working for fade out animation. What should I do now?
Try like this to hide it with animation, use any method of your choice in option parameter:
UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveEaseOut, animations: {
self.myMarker.opacity = 0.0
}, completion: { (true) in
self.myMarker.map = nil
})
When there are several markers or some code snippet depend on that marker, and you need to remove them synchronously then the process you should be done in background thread. But the background thread can't update the UI.
So, the UI update portion you need do in main thread. Like I have done here,
//Swift 3.1
DispatchQueue.global(qos: .background).async {
//HERE MAY HAVE SOME DEPENDENT CODE
DispatchQueue.main.async {
UIView.animate(withDuration: 0.5, animations: {
self.myMarker.marker?.opacity = 0.0
}, completion: { (yes) in
self.myMarker.marker?.map = nil
})
}
//HERE MAY HAVE SOME DEPENDENT CODE
}
There are so many posts similar to this that I have seen and none of them works.
Here is my code so far.
func startRotating(view : UIImageView, rotating : Bool) {
//Rotating the rotatingClockBarImage
UIView.animate(withDuration: 1.0, delay: 0.0, options: [.curveLinear], animations: {
view.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
}, completion: {finished in
UIView.animate(withDuration: 1.0, delay: 0.0, options: [.curveLinear], animations: {
view.transform = CGAffineTransform(rotationAngle: 0)
}, completion: nil)
})//End of rotation
continueRotating(view: view)
}
The original problem would be that I couldn't rotate a full 360 degrees. I figured that out by rotating half way and the other half in the completion.
The problem now is once this animation finishes, that's it. I have tried putting it in a while loop, for loop, calling two similar functions back and forth. Nothing works it just keeps freezing my app.
In a for loop, for example, that would run 3 times, I put a print(). The print writes to the console three times but the animation only happens once. Because of this I am thinking the animation is just cutting itself off before it even starts, and the final rotation is the only one that plays through. So I need to find a way to allow it to play each rotation through.
This shouldn't be that hard seeing that Apple had their planes rotate so easily in a former version of Xcode in a game app. I'm trying to avoid deleting and reinstalling the old version just so I can look at that code.
Actually it would be more easy:
extension UIView {
func rotate360Degrees(duration: CFTimeInterval = 1.0) {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat.pi * 2
rotateAnimation.duration = duration
rotateAnimation.repeatCount = Float.infinity
self.layer.add(rotateAnimation, forKey: nil)
}
func stopRotating(){
self.layer.sublayers?.removeAll()
//or
self.layer.removeAllAnimations()
}
}
Then for rotating:
yourView.rotate360Degrees()
for stopping:
yourView. stopRotating()
Did you try calling startRotating again in the completion block of your second half turn ?
Note that you should do that conditionally with a "stop" flag of your own if you ver want it to stop.
Im trying to take my animating function and turn it into its own class.
Can someone help me figure out how to add repeat functionallity? Ive been trying for two days with no luck.
class FlipAnimator {
class func animate(view: UIView) {
let view = view
view.layer.transform = FlipAnimatorStartTransform
view.layer.opacity = 0.8
UIView.animateWithDuration(0.5) {
view.layer.transform = CATransform3DIdentity
view.layer.opacity = 1
}
}
Look at the available signatures for animateWithDuration here. Note that one of them is animateWithDuration(_:delay:options:animations:completion:). Try using .Repeat as the options parameter (a full list of options can be found here).
Specifically:
UIView.animateWithDuration(0.5, delay: 0, options: .Repeat, {
view.layer.transform = CATransform3DIdentity
view.layer.opacity = 1
}, nil)
Note that because animations is not the last parameter you cannot use the trailing closure
syntax.