I have a function I am using to animate 4 UILabels. The animation is chained and I am trying to reduce repeated code by creating a recursive function that accepts the label as an argument, on completion calls itself with the next label.
fileprivate func handleAnimations(firstLabel: UILabel, secondLabel: UILabel) -> Void {
UIView.animate(withDuration: 1, delay: 2, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseInOut, animations: {
firstLabel.alpha = 1
}) { (_) in
UIView.animate(withDuration: 1, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseInOut, animations: {
secondLabel.alpha = 1
}) { (_) in
UIView.animate(withDuration: 0.5, delay: 3, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: .curveEaseInOut, animations: {
firstLabel.alpha = 0
secondLabel.alpha = 0
}, completion: { (_) in
self.handleAnimations(firstLabel: self.introTextLabelThree, secondLabel: self.introTextLabelFour)
})
}
}
}
Is this the best way to achieve this? If so, how I prevent the final complete block calling itself over and over with the final 2 labels?
The effect I am trying to achieve is:
labelOne - fades in
labelTwo - fades in
labelOne and labelTwo - fade out
labelThree - fades in
labelFour - fades in
labelThree and labelFour - fade out
After labelThree and labelFour I would be looking to implement another function call.
Do not do this. Please use animateKeyframes. This will allow you to properly chain animations without the dreaded pyramid of doom.
UIView.animateKeyframes(withDuration: 5.0, delay: 0, options: [.calculationModeCubic], animations: {
// Add animations
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1.0/6.0, animations: {
self.introTextLabel.alpha = 1
})
UIView.addKeyframe(withRelativeStartTime: 1.0/6.0, relativeDuration: 1.0/5.0, animations: {
self.introTextLabelTwo.alpha = 1
})
UIView.addKeyframe(withRelativeStartTime: 2.0/6.0, relativeDuration: 1.0/5.0, animations: {
self.introTextLabel.alpha = 0
self.introTextLabelTwo.alpha = 0
})
UIView.addKeyframe(withRelativeStartTime: 3.0/6.0, relativeDuration: 1.0/5.0, animations: {
self.introTextLabelThree.alpha = 1
})
UIView.addKeyframe(withRelativeStartTime: 4.0/6.0, relativeDuration: 1.0/5.0, animations: {
self.introTextLabelFour.alpha = 1
})
UIView.addKeyframe(withRelativeStartTime: 5.0/6.0, relativeDuration: 1.0/5.0, animations: {
self.introTextLabelThree.alpha = 0
self.introTextLabelFour.alpha = 0
})
}, completion:{ _ in
// fire off whatever other method you want here
})
Related
I'm having an issue trying to touch a label that keeps animating because during the animation it becomes an image (set the animation on repeat). Is there a way to touch it during animation (I cant't use UITapGestureRecognizer) ?
func animatePro() {
UIView.animate(withDuration: 2, delay: 0, options: [.autoreverse, .repeat], animations: {
//1.5 times it's normal size
self.proLabel.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
}){ (finished) in
self.proLabel
.transform = CGAffineTransform.identity
}
}
func animateRev() {
UIView.animate(withDuration: 2, delay: 0, options: [.repeat], animations: {
//1.5 times it's normal size
self.revLabel.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
}){ (finished) in
UIView.animate(withDuration: 2, delay: 0, animations: {
self.revLabel.transform = CGAffineTransform.identity
})
}
}
Add allowUserInteraction
options: [.allowUserInteraction,.autoreverse, .repeat]
I would like to animate the whole chain for forever.
UIView.animate(withDuration: 2.0, delay: 0.2, options: .curveEaseOut, animations: {
self.circleImageView.transform = CGAffineTransform(scaleX: 9, y: 9)
}) { (_) in
UIView.animate(withDuration: 2.0, delay: 0.2, options: .curveEaseIn, animations: {
self.circleImageView.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
}, completion: nil)
}
You can wrap the two animations within a keyframe animation. Then you can .repeat that:
UIView.animateKeyframes(withDuration: 4.4, delay: 0, options: .repeat, animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 2.0/4.4) {
self.circleImageView.transform = .init(scaleX: 9, y: 9)
}
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 2.0/4.4) {
self.circleImageView.transform = .init(scaleX: 0.01, y: 0.01)
}
})
Wrap it up inside a method:
func repeatingAnimation() {
UIView.animate(withDuration: 2.0, delay: 0.2, options: .curveEaseOut, animations: {
self.circleImageView.transform = CGAffineTransform(scaleX: 9, y: 9)
}) { (_) in
UIView.animate(withDuration: 2.0, delay: 0.2, options: .curveEaseIn, animations: {
self.circleImageView.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
}, completion: { [weak self] _ in
self?.repeatingAnimation()
})
}
}
I've added this animation code to the end of a function that is called when the button is pushed, but while the animation is happening, it ignores the set target selector until the animation is finished. The animation is very fast, but I'd like users to be able to press it rapidly.
let transforms: CGAffineTransform = .identity
mirroredButton.transform = transforms
UIView.animate(withDuration: 0.05, animations: {
mirroredButton.transform = transforms.scaledBy(x: 0.75, y: 0.75)
},
completion: { _ in
UIView.animate(withDuration: 0.1) {
mirroredButton.transform = transforms.scaledBy(x: 1.0, y: 1.0)
}
})
Update:
Using the answer, I updated my animation code as shown below. Both animation calls need options. The 2nd has a nil completion handler.
let transforms: CGAffineTransform = .identity
mirroredButton.transform = transforms
UIView.animate(withDuration: 0.05, delay: 0.0, options: .allowUserInteraction, animations: {
mirroredButton.transform = transforms.scaledBy(x: 0.75, y: 0.75)
},
completion: { _ in
UIView.animate(withDuration: 0.1, delay: 0.0, options: .allowUserInteraction, animations: {
mirroredButton.transform = transforms.scaledBy(x: 1.0, y: 1.0)
}, completion:nil)
})
User interaction is disabled for the duration of a view's animation. If it's critical that the user should interact with a view during an animation, you can pass in the option .allowUserInteraction, like so:
UIView.animate(withDuration: 1.0, delay: 0.0, options: .allowUserInteraction, animations: {
//animate here
})
I'm trying to understand how to use UIView.animateKeyframes however I cannot for the life of me understand how to calculate the timings / durations.
I am trying to achieve the following:
I expect this entire animation to last 10 seconds, made up of...
first addKeyframe runs at 01 seconds and takes 1 second
second addKeyframe runs at 02 seconds and takes 1 second
third addKeyframe runs at 05 seconds and takes 2 seconds
fourth addKeyframe runs at 07 seconds and takes 1 second
fourth addKeyframe runs at 09 seconds and takes 1 second
fileprivate func animateWelcomeText() -> Void {
let duration: TimeInterval = 10
UIView.animateKeyframes(withDuration: duration, delay: 0, options: .calculationModeLinear, animations: {
UIView.addKeyframe(withRelativeStartTime: 1.0, relativeDuration: 1.0, animations: {
self.introTextLabel.alpha = 1
})
UIView.addKeyframe(withRelativeStartTime: 2.0, relativeDuration: 1.0, animations: {
self.introTextLabelTwo.alpha = 1
})
UIView.addKeyframe(withRelativeStartTime: 5.0, relativeDuration: 2.0, animations: {
self.introTextLabel.alpha = 0
self.introTextLabelTwo.alpha = 0
})
UIView.addKeyframe(withRelativeStartTime: 7.0, relativeDuration: 1.0, animations: {
self.introTextLabelThree.alpha = 1
})
UIView.addKeyframe(withRelativeStartTime: 9.0, relativeDuration: 1.0, animations: {
self.introTextLabelFour.alpha = 1
})
}) { (_) in
print("Complete")
}
}
Nothing happens and then the animation completes and the last 2 items suddenly appear.
It's just math. Just divide all the values by your duration to get the relative values.
fileprivate func animateWelcomeText() -> Void {
let duration: TimeInterval = 10
UIView.animateKeyframes(withDuration: duration, delay: 0, options: .calculationModeLinear, animations: {
UIView.addKeyframe(withRelativeStartTime: 1/duration, relativeDuration: 1/duration, animations: {
self.introTextLabel.alpha = 1
})
UIView.addKeyframe(withRelativeStartTime: 2/duration, relativeDuration: 1/duration, animations: {
self.introTextLabelTwo.alpha = 1
})
UIView.addKeyframe(withRelativeStartTime: 5/duration, relativeDuration: 2/duration, animations: {
self.introTextLabel.alpha = 0
self.introTextLabelTwo.alpha = 0
})
UIView.addKeyframe(withRelativeStartTime: 7/duration, relativeDuration: 1/duration, animations: {
self.introTextLabelThree.alpha = 1
})
UIView.addKeyframe(withRelativeStartTime: 9/duration, relativeDuration: 1/duration, animations: {
self.introTextLabelFour.alpha = 1
})
}) { (_) in
print("Complete")
}
}
You have your values out of x (10). The method needs it out of 1.
I'm porting an older app over to Xcode 7 beta and I'm getting an error on my animations:
Cannot invoke 'animateWithDuration' with an argument list of type
'(Double, delay: Double, options: nil, animations: () -> _,
completion: nil)'
Here's the code:
UIView.animateWithDuration(0.5, delay: 0.3, options: nil, animations: {
self.username.center.x += self.view.bounds.width
}, completion: nil)
This works in Xcode 6 so I'm assuming this is an update in Swift. So my question is:
What's the Swift 3 syntax for animateWithDuration?
Swift 3/4 Syntax
Here's an update with the Swift 3 Syntax:
UIView.animate(withDuration: 0.5, delay: 0.3, options: [.repeat, .curveEaseOut, .autoreverse], animations: {
self.username.center.x += self.view.bounds.width
}, completion: nil)
If you need to add a completion handler just add a closure like so:
UIView.animate(withDuration: 0.5, delay: 0.3, options: [.repeat, .curveEaseOut, .autoreverse], animations: {
// animation stuff
}, completion: { _ in
// do stuff once animation is complete
})
Old Answer:
It turns out it's a very simple fix, just change options: nil to options: [].
Swift 2.2 Syntax:
UIView.animateWithDuration(0.5, delay: 0.3, options: [], animations: {
self.username.center.x += self.view.bounds.width
}, completion: nil)
What changed?
Swift 2 got rid of the C-Style comma-delimited list of options in favor of option sets (see: OptionSetType). In my original question, I passed in nil for my options, which was valid prior to Swift 2. With the updated syntax, we now see an empty option list as an empty set: [].
An example of animateWithDuration with some options would be this:
UIView.animateWithDuration(0.5, delay: 0.3, options: [.Repeat, .CurveEaseOut, .Autoreverse], animations: {
self.username.center.x += self.view.bounds.width
}, completion: nil)
Swift 3, 4, 5
UIView.animate(withDuration: 1.5, delay: 0.05 * Double(index), usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: [], animations: {
cell.transform = CGAffineTransform(translationX: 0, y: 0)
}, completion: nil)
Swift 3 Syntax with completion block
UIView.animate(withDuration: 3.0 , delay: 0.25, options: .curveEaseOut, animations: {
// animation
}, completion: { _ in
// completion
})
Swift 2
UIView.animateWithDuration(1.0, delay: 0.1, options: [.Repeat, .CurveEaseOut, .Autoreverse], animations: {
// animation
}, completion: { finished in
// completion
})
Swift 3, 4, 5
UIView.animate(withDuration: 1.0, delay: 0.1, options: [.repeat, .curveEaseOut, .autoreverse], animations: {
// animation
}, completion: { finished in
// completion
})