This will be a 'best way to do' kind of a question.
I know there are multiple ways to do sequence animations, but which one is the better way to do it?
let's say I have 3 animations that I'm trying to execute, one after the other, so the next one only starts when the previous has finished.
I can do it with the UIView.animate(withCompletionBlock:) so in the completion section I call the second one and on the completionBlock of the second one I call the third one.
This one in particular makes me think: Is that a 'beautiful' way to do it? Is there any known issues using completionBlocks within another?
Just a thought. Calling animations within the previous animation completion block, may bring the code somehow like the pyramid of doom, if there's something like 10 synchronous animations, don't you think?
Another way to do is with the dispatch_after, the I call the next animation based on the NSTimeInterval of the previous one. In this case it may cause a app crash suspending the Main thread for the next one to execute, then suspend it again to execute the third one.
The last known way that I know is that I can use a NSTimeInterval to call the next animation, but this one is somewhat like the dispatch_after option.
In your opinion which would be the best way to do it?
Executing 3 or more visual animations, all synchronously (waiting for the previous one to finish to start the next one).
I might use this a lot in my app and I want to start it with the best way.
EXAMPLES
Animations within completionBlocks
DispatchQueue.main.async {
UIView.animate(withDuration: 2.0, animations: {
view1.alpha = 1
}, completion: { (_) in
UIView.animate(withDuration: 5.0, animations: {
view2.alpha = 1
}, completion: { (_) in
UIView.animate(withDuration: 2.0, animations: {
view3.alpha = 0
}, completion: { (_) in
UIView.animate(withDuration: 2.0, animations: {
view4.alpha = 0
}, completion: { (_) in
print("all completed")
})
})
})
})
}
With dispatch_after:
let anim1_duration = 2.0
let anim2_duration = 3.0
let anim3_duraiton = 5.0
UIView.animate(withDuration: anim1_duration, animations: {
view1.alpha = 1
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.init(uptimeNanoseconds: UInt64(anim1_duration * Double(NSEC_PER_SEC)))) {
UIView.animate(withDuration: anim2_duration, animations: {
view2.alpha = 1
}
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.init(uptimeNanoseconds: UInt64((anim1_duration + anim2_duration) * Double(NSEC_PER_SEC)))) {
UIView.animate(withDuration: anim3_duration, animations: {
view3.alpha = 1
}
}
Check out UIView's animateKeyframes(withDuration:delay:options:animations:completion:):
class func animateKeyframes(withDuration duration: TimeInterval,
delay: TimeInterval,
options: UIViewKeyframeAnimationOptions = [],
animations: #escaping () -> Void,
completion: ((Bool) -> Void)? = nil)
For your specific case, it would look like this:
UIView.animateKeyframes(withDuration: 20,
delay: 0,
options: [],
animations: {
UIView.addKeyframe(withRelativeStartTime: 0,
relativeDuration: 0.2,
animations: {
view1.alpha = 1
})
UIView.addKeyframe(withRelativeStartTime: 0.2,
relativeDuration: 0.3,
animations: {
view2.alpha = 1
})
UIView.addKeyframe(withRelativeStartTime: 0.5,
relativeDuration: 0.1,
animations: {
view3.alpha = 0
})
UIView.addKeyframe(withRelativeStartTime: 0.6,
relativeDuration: 0.4,
animations: {
view4.alpha = 0
})
},
completion: nil)
In terms of 'better reading code' consider these 2 ways of performing animations:
building stacks of animations: more
create keyframe animations using CAKeyframeAnimation: more
These options are avoiding completion blocks, which I like.
Related
Goal: Create a looping animation with 2 overlapping uilabels to show one and hide the other by setting alphas respectively to 1 and 0.
Any tips would be appreciated.
What I've Tried:
Autoreverse/repeat animation - While this works, there is no delay in-between when the animation reverses and repeats itself.
DispatchQueue.main.async {
UIView.animate(withDuration: 1.0, delay: 3.0, options: [.autoreverse, .repeat]) {
self.labelOne.alpha = 0
self.labelTwo.alpha = 1
}
}
Recursive animation - This works, but when I re-enter the foreground from the background, the animation spazzes out showing/hiding each uilabel. My issue here is being unable to stop/reset the recursive animation.
DispatchQueue.main.async {
UIView.animate(withDuration: 1.0, delay: 3.0, options: .curveLinear) {
self.labelOne.alpha = 0
self.labelTwo.alpha = 1
} completion: { isComplete in
UIView.animate(withDuration: 1.0, delay: 3.0, options: [.curveLinear]) {
self.labelOne.alpha = 1
self.labelTwo.alpha = 0
} completion: { isComplete in
self.runThisAnimation()
}
}
}
To animate a bar opening...
#IBOutlet var barHeight: NSLayoutConstraint!
barHeight.constant = barShut?30:100
self.view.layoutIfNeeded()
t = !barShut?30:100
UIView.animate(withDuration: 0.15,
delay: 0,
options: UIViewAnimationOptions.curveEaseOut,
animations: { () -> Void in
self.barHeight.constant = t
self.view.layoutIfNeeded()
},
completion: {_ in
Screen.barShut = !Screen.barShut
}
)
That's great ...
But how would you make it boing like this?
(The only way I'd know to do this is, use CADisplayLink, with a few lines of code for a spring decaying.) Is this available in UIKit?
You can use the spring animation method that is built in to UIView:
func toggleBar() -> Void {
self.view.layoutIfNeeded()
let newHeight:CGFloat = !barShut ? 30:100
barShut = !barShut
barHeightConstraint.constant = newHeight
UIView.animate(withDuration: 1.5, delay: 0, usingSpringWithDamping: 0.2, initialSpringVelocity: 3, options: [], animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
You will want a longer animation duration than 0.15 of a second in order for the bounce to seem realistic; I think the values I have look pretty good, but you can play with them to get the exact effect you are after.
Since the animation duration is longer, I found that I could tap the button the triggered the open/shut while the previous animation was still running. Setting barShut in the completion block meant that the bar didn't react to all taps. I moved the toggle outside of the animation to address this.
I'm creating a simple left to right animation for a label using key frames but when the animation repeats, the delay is ignored.
The first time it executes, the delay of 3 seconds has an effect, but when the animation repeats, the delay is ignored. This causes the animation to re-start immediately after it ends.
UIView.animateKeyframes(withDuration: 10, delay: 3, options: [.calculationModePaced, .repeat], animations: {
let xDist = self.Label_ArtistAlbum2.frame.origin.x
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.1, animations: {
self.Label_ArtistAlbum2.frame.origin.x = self.Label_ArtistAlbum2.frame.origin.x - (xDist * 0.1)
})
UIView.addKeyframe(withRelativeStartTime: 0.9, relativeDuration: 0.1, animations: {
self.Label_ArtistAlbum2.frame.origin.x = 0
})
}, completion: nil)
I've tried adding an extra keyframe at the end however this has no effect even with the altered times:
UIView.animateKeyframes(withDuration: 10, delay: 3, options: [.calculationModePaced, .repeat], animations: {
let xDist = self.Label_ArtistAlbum2.frame.origin.x
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.1, animations: {
self.Label_ArtistAlbum2.frame.origin.x = self.Label_ArtistAlbum2.frame.origin.x - (xDist * 0.1)
})
UIView.addKeyframe(withRelativeStartTime: 0.1, relativeDuration: 0.7, animations: {
self.Label_ArtistAlbum2.frame.origin.x = 0
})
//attempted pause - does not appear to work perhaps since the position is unchanged?
UIView.addKeyframe(withRelativeStartTime: 0.8, relativeDuration: 0.2, animations: {
self.Label_ArtistAlbum2.frame.origin.x = 0
})
}, completion: nil)
If the delay will not be repeated along with the rest of the animation, how can I create a pause before the entire animation repeats?
I had a similar problem for animating a loading view. I solved it this way:
I created an enum for the steps in the animation
private enum TriangleToAnimate {
case one
case two
case three
case pause
}
I have my variables
private var triangleViewToFireCount = TriangleToAnimate.one
var triangle1View : TriangleView
var triangle2View : TriangleView
var triangle3View : TriangleView
I start a timer to run each animation
override init(frame: CGRect) {
Timer.scheduledTimer(timeInterval: 0.33, target: self, selector: #selector(LoadingView.timerFire), userInfo: nil, repeats: true)
}
For the selector I have a fire method. In the method I have a switch for each of the enum cases
func timerFire(){
let anim = createAnimation()
switch triangleViewToFireCount {
case .one:
triangle1View.layer.add(anim, forKey: "transform")
triangleViewToFireCount = .two
case .two:
triangle2View.layer.add(anim, forKey: "transform")
triangleViewToFireCount = .three
case .three:
triangle3View.layer.add(anim, forKey: "transform")
triangleViewToFireCount = .pause
default:
triangleViewToFireCount = .one
}
}
This is the code how I created the animation as a keyframe
func createAnimation() -> CAKeyframeAnimation{
let tr = CATransform3DIdentity
let orignalScale = CATransform3DScale(tr, 1, 1, 1)
let doubleScale = CATransform3DScale(tr, 2, 2, 1)
let keyAn = CAKeyframeAnimation(keyPath: "transform")
keyAn.keyTimes = [0, 0.1, 0.6]
keyAn.duration = 1
keyAn.values = [orignalScale,doubleScale,orignalScale]
keyAn.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
return keyAn
}
I've done a lot of testing on this issue and have found what I believe is the problem.
I think that the animation option for the Curve is my issue.
In my case choosing
calculationModePaced
has the effect of recalculating my keyframe parameters and not guaranteeing to hit any of them except the beginning and end. All intermediate keyframes become 'suggestions'.
You can't create a pause at the end because the keyframe is 'consumed' when recalculating and does not stand on it's own.
I changed to calculationModeLinear and got the keyframes I expected, and the pause too.
However, it is as smooth as I would like so I'll have to keep tinkering...
Apple's docs are here - they're descriptive but could really use some graphics and/or examples:
https://developer.apple.com/reference/uikit/uiview?language=swift
A good graph of the curves can be found here: https://www.shinobicontrols.com/blog/ios7-day-by-day-day-11-uiview-key-frame-animations
I am trying to animate a few circles and changing their opacities simultaneously.
UIView.animateKeyframesWithDuration(2, delay: 0, options: .Repeat, animations: {
UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0.5, animations: {
self.innerRingView.alpha = 1
})
UIView.addKeyframeWithRelativeStartTime(0.5, relativeDuration: 0.5, animations: {
self.innerRingView.alpha = 0
self.middleRingView.alpha = 1
})
UIView.addKeyframeWithRelativeStartTime(1, relativeDuration: 0.5, animations: {
self.middleRingView.alpha = 0
self.outterRingView.alpha = 1
})
UIView.addKeyframeWithRelativeStartTime(1.5, relativeDuration: 0.5, animations: {
self.outterRingView.alpha = 0
})
}, completion: nil)
For some reason, it never gets to the second animation:
UIView.addKeyframeWithRelativeStartTime(1, relativeDuration: 0.5, animations: {
self.middleRingView.alpha = 0
self.outterRingView.alpha = 1
})
Because of this, my animation isn't work. innerRingView never goes back to 0 opacity and outterRingView never appears (all the views are set to 0 alpha by default).
What seems to be the problem here?
Issue appeared to be with your key frame relative start time
UIView.addKeyframeWithRelativeStartTime
which must be be in the range 0 to 1, where 0 represents the start of the overall animation and 1 represents the end of the overall animation. For example, for an animation that is two seconds in duration, specifying a start time of 0.5 causes the animations to begin executing one second after the start of the overall animation.
When I animate a change to a view's transform, then reset that change in another animation before the first animation finishes, everything's great (shown here with a rotation). The animation smoothly switches to the new target:
But when I do this with a scale, the animation overshoots magnificently:
Here's the breaking code:
UIView.animateWithDuration(1) {
self.someView.layer.transform = CATransform3DMakeScale(0.001, 0.001, 1)
}
UIView.animateWithDuration(1,
delay: 0.5,
options: nil,
animations: {
self.someView.layer.transform = CATransform3DIdentity
}, completion: nil
)
Has anyone else seen this? Am I doing something wrong?
EDIT: And is there a good workaround?
EDIT 2: I believe this is a duplicate of this question and am voting to close.
This blog post provides the answer: in iOS 8, UIView animations are additive, and this has an unfortunate result with scale animations.
Basically, the second animation happens together with the first animation. The only solution is to explicitly remove the original animation before starting a new one:
view.layer.transform = view.layer.presentationLayer().transform
view.layer.removeAllAnimations()
Hi I'm not quite sure what your looking for but if you want the view to go back to it's original scale you'd add the .Autoreverse flag.
UIView.animateWithDuration(1, delay: 0, options: .Autoreverse | .Repeat, animations: {
myView.layer.transform = CATransform3DMakeScale(0.001, 0.001, 1)
}, completion: nil)
While if you wanted to string animations together I'd do it within UIView.animateKeyframesWithDuration()
UIView.animateKeyframesWithDuration(2, delay: 0.0, options: nil, animations: {
UIView.addKeyframeWithRelativeStartTime(0.0, relativeDuration: 0.5, animations: {
// Animation 1
})
UIView.addKeyframeWithRelativeStartTime(1, relativeDuration: 0.5, animations: {
// Animation 2
})
}, completion: nil)