Rotate UIButton 360 degrees - ios

I've been trying to run an animation that rotates my UIButton 360 degrees using this code:
UIView.animateWithDuration(3.0, animations: {
self.vineTimeCapButton.transform = CGAffineTransformMakeRotation(CGFloat(M_PI*2))
self.instagramTimeCapButton.transform = CGAffineTransformMakeRotation(CGFloat(M_PI*2))
})
However, it doesn't rotate 360 degrees because the UIButton is already at that location.
How can I rotate my UIButton 360 degrees?

You can use a trick: start rotating with 180 degrees first and then rotate with 360 degrees. Use 2 animations with delay.
Try this.
UIView.animate(withDuration: 0.5) {
self.view.transform = CGAffineTransform(rotationAngle: .pi)
}
UIView.animate(
withDuration: 0.5,
delay: 0.45,
options: UIView.AnimationOptions.curveEaseIn
) {
self.view.transform = CGAffineTransform(rotationAngle: 2 * .pi)
}

As discussed here, you can also use a CAAnimation. This code applies one full 360 rotation:
Swift 3
let fullRotation = CABasicAnimation(keyPath: "transform.rotation")
fullRotation.delegate = self
fullRotation.fromValue = NSNumber(floatLiteral: 0)
fullRotation.toValue = NSNumber(floatLiteral: Double(CGFloat.pi * 2))
fullRotation.duration = 0.5
fullRotation.repeatCount = 1
button.layer.add(fullRotation, forKey: "360")
You will need to import QuartzCore:
import QuartzCore
And your ViewController needs to conform to CAAnimationDelegate:
class ViewController: UIViewController, CAAnimationDelegate {
}

Swift 4: Animation nested closure is better than animation delay block.
UIView.animate(withDuration: 0.5, animations: {
button.transform = CGAffineTransform(rotationAngle: (CGFloat(Double.pi)))
}) { (isAnimationComplete) in
// Nested Block
UIView.animate(withDuration: 0.5) {
button.transform = CGAffineTransform(rotationAngle: (CGFloat(Double.pi * 2)))
}
}

Swift 4 version that allows you to rotate the same view infinite times:
UIView.animate(withDuration: 0.5) {
self.yourButton.transform = self.yourButton.transform.rotated(by: CGFloat.pi)
}
if you want to rotate 360°:
UIView.animate(withDuration: 0.5) {
self.yourButton.transform = self.yourButton.transform.rotated(by: CGFloat.pi)
self.yourButton.transform = self.yourButton.transform.rotated(by: CGFloat.pi)
}

in Swift 3 :
UIView.animate(withDuration:0.5, animations: { () -> Void in
button.transform = CGAffineTransform(rotationAngle: CGFloat(M_PI))
})
UIView.animate(withDuration: 0.5, delay: 0.45, options: .curveEaseIn, animations: { () -> Void in
button.transform = CGAffineTransform(rotationAngle: CGFloat(M_PI * 2))
}, completion: nil)

You can use a little extension based on CABasicAnimation (Swift 4.x):
extension UIButton {
func rotate360Degrees(duration: CFTimeInterval = 1.0, completionDelegate: AnyObject? = nil) {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat(.pi * 2.0)
rotateAnimation.duration = duration
if let delegate: AnyObject = completionDelegate {
rotateAnimation.delegate = delegate as? CAAnimationDelegate
}
self.layer.add(rotateAnimation, forKey: nil)
}
}
Usage:
For example we can start to make a simple button:
let button = UIButton()
button.frame = CGRect(x: self.view.frame.size.width/2, y: 150, width: 50, height: 50)
button.backgroundColor = UIColor.red
button.setTitle("Name your Button ", for: .normal)
button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
self.view.addSubview(button)
Then we can build its selector:
#objc func buttonAction(sender: UIButton!) {
print("Button tapped")
sender.rotate360Degrees()
}

You can also try to implement an extension which will simplify things if you need to do it multiple time
extension UIView {
func rotate360Degrees(duration: CFTimeInterval = 1, repeatCount: Float = .infinity) {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat(Double.pi * 2)
rotateAnimation.isRemovedOnCompletion = false
rotateAnimation.duration = duration
rotateAnimation.repeatCount = repeatCount
layer.add(rotateAnimation, forKey: nil)
}
// Call this if using infinity animation
func stopRotation () {
layer.removeAllAnimations()
}
}

Swift 5. Try this, it worked for me
UIView.animate(withDuration: 3) {
self.yourButton.transform = CGAffineTransform(rotationAngle: .pi)
self.yourButton.transform = CGAffineTransform(rotationAngle: .pi * 2)
}

Related

Convert UIView animate to CAKeyframeAnimation in swift

I'm working on a Swift animation and I'm currently trying to refactor the animation code since my I'm trying to use the CAkeyframeAnimation but now I struggle with how to convert UIView animate method to CAKeyframeAnimation.
This is the one using UIView animate function.
class myCustomView: UIView {
func showPointOfInterest(at point: CGPoint, hideViewAfterAnimation: Bool) {
if isPreviousAnimationPresenting() { layer.removeAllAnimations() }
center = point
transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
self.alpha = 1
UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseInOut]) {
self.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
} completion: { animated in
if !animated {
return
} else {
UIView.animate(withDuration: 0.1, delay: 0, options: [.curveEaseInOut]) {
self.transform = .identity
} completion: { animated in
if !animated {
return
} else {
UIView.animate(withDuration: 1.5, delay: 0, options: [.curveEaseInOut]) {
self.alpha = 0.5
} completion: { animated in
if !animated {
return
} else {
UIView.animate(withDuration: 0.5, delay: 0, options: [.curveEaseInOut]) {
self.alpha = hideViewAfterAnimation ? 0 : 0.5
}
}
}
}
}
}
}
}
Generally, these are the things I struggled with...
I'm not sure what is the keyPath is gonna be.
In my UIView animate function, I change the transform and alpha value. I saw examples put string for the keyPath, like CAKeyframeAnimation(keyPath: "position.x"), so what should I put for my keyPath?
Since I want to change the size of the view by using CGAffineTransform in the UIView animate function, do I put CGAffineTransform(scaleX:, y: ) in the transformKeyframeAnimation.values?
func showPointOfInterest(at point: CGPoint, hideViewAfterAnimation: Bool) {
if isPreviousAnimationPresenting() { layer.removeAllAnimations() }
center = point
transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
alpha = 1
let transformKeyframeAnimation = CAKeyframeAnimation(keyPath: "transform")
transformKeyframeAnimation.values = [CGAffineTransform(scaleX: 0.9, y: 0.9), CGAffineTransform(scaleX: 1, y: 1)]
transformKeyframeAnimation.duration = 1.0
transformKeyframeAnimation.isRemovedOnCompletion = hideViewAfterAnimation
self.layer.add(transformKeyframeAnimation, forKey: "transform")
}

Rotate a view 360 degrees infinitely

I am using this code to rotate a view 360 degrees infinitely. But my view is not rotating:
func rotateImageView() {
UIView.animate(withDuration: 3.0, delay: 0, options: [.repeat, .curveLinear], animations: {
self.vinylView.transform = CGAffineTransform(rotationAngle: .pi * 2)
})
}
How to fix it?
Substitute pi * with /, delete repeat, add completion and recall the function like this:
private func rotateImageView() {
UIView.animate(withDuration: 3, delay: 0, options: .curveLinear, animations: {
self.vinylView.transform = self.vinylView.transform.rotated(by: .pi / 2)
}) { (finished) in
if finished {
self.rotateImageView()
}
}
}
Solution using CABasicAnimation
// Rotate vinvlView
vinylView.layer.add(CABasicAnimation.rotation, forKey: nil)
extension CABasicAnimation {
static let rotation : CABasicAnimation = {
let animation = CABasicAnimation(keyPath: "transform.rotation.z")
animation.repeatCount = .infinity // Rotate a view 360 degrees infinitely
animation.fromValue = 0
animation.toValue = CGFloat.pi * 2
animation.duration = 3.0
return animation
}()
}

How to shake the text field in swift 4? [duplicate]

I am trying to figure out how to make the text Field shake on button press when the user leaves the text field blank.
I currently have the following code working:
if self.subTotalAmountData.text == "" {
let alertController = UIAlertController(title: "Title", message:
"What is the Sub-Total!", preferredStyle: UIAlertControllerStyle.Alert)
alertController.addAction(UIAlertAction(title: "Okay", style: UIAlertActionStyle.Default,handler: nil))
self.presentViewController(alertController, animated: true, completion: nil)
} else {
}
But i think it would be much more appealing to just have the text field shake as an alert.
I can't find anything to animate the text field.
Any ideas?
Thanks!
You can change the duration and repeatCount and tweak it. This is what I use in my code. Varying the fromValue and toValue will vary the distance moved in the shake.
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.07
animation.repeatCount = 4
animation.autoreverses = true
animation.fromValue = NSValue(cgPoint: CGPoint(x: viewToShake.center.x - 10, y: viewToShake.center.y))
animation.toValue = NSValue(cgPoint: CGPoint(x: viewToShake.center.x + 10, y: viewToShake.center.y))
viewToShake.layer.add(animation, forKey: "position")
The following function is used in any view.
extension UIView {
func shake() {
let animation = CAKeyframeAnimation(keyPath: "transform.translation.x")
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.duration = 0.6
animation.values = [-20.0, 20.0, -20.0, 20.0, -10.0, 10.0, -5.0, 5.0, 0.0 ]
layer.add(animation, forKey: "shake")
}
}
EDIT: using CABasicAnimation cause the app to crash if you ever trigger the animation twice in a row. So be sure to use CAKeyframeAnimation. Bug has been fixed, thanks to the comments :)
Or you can use this if you want more parameters (in swift 5) :
public extension UIView {
func shake(count : Float = 4,for duration : TimeInterval = 0.5,withTranslation translation : Float = 5) {
let animation = CAKeyframeAnimation(keyPath: "transform.translation.x")
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.repeatCount = count
animation.duration = duration/TimeInterval(animation.repeatCount)
animation.autoreverses = true
animation.values = [translation, -translation]
layer.add(animation, forKey: "shake")
}
}
You can call this function on any UIView, UIButton, UILabel, UITextView etc. This way
yourView.shake()
Or this way if you want to add some custom parameters to the animation:
yourView.shake(count: 5, for: 1.5, withTranslation: 10)
I think all of these are dangerous.
If your shake animation is based on a user action and that user action is triggered while animating.
CRAAAAAASH
Here is my way in Swift 4:
static func shake(view: UIView, for duration: TimeInterval = 0.5, withTranslation translation: CGFloat = 10) {
let propertyAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 0.3) {
view.transform = CGAffineTransform(translationX: translation, y: 0)
}
propertyAnimator.addAnimations({
view.transform = CGAffineTransform(translationX: 0, y: 0)
}, delayFactor: 0.2)
propertyAnimator.startAnimation()
}
Maybe not the cleanest, but this method can be triggered repeatedly and is easily understood
Edit:
I am a huge proponent for usage of UIViewPropertyAnimator. So many cool features that allow you to make dynamic modifications to basic animations.
Here is another example to add a red border while the view is shaking, then removing it when the shake finishes.
static func shake(view: UIView, for duration: TimeInterval = 0.5, withTranslation translation: CGFloat = 10) {
let propertyAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 0.3) {
view.layer.borderColor = UIColor.red.cgColor
view.layer.borderWidth = 1
view.transform = CGAffineTransform(translationX: translation, y: 0)
}
propertyAnimator.addAnimations({
view.transform = CGAffineTransform(translationX: 0, y: 0)
}, delayFactor: 0.2)
propertyAnimator.addCompletion { (_) in
view.layer.borderWidth = 0
}
propertyAnimator.startAnimation()
}
Swift 5.0
extension UIView {
func shake(){
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.07
animation.repeatCount = 3
animation.autoreverses = true
animation.fromValue = NSValue(cgPoint: CGPoint(x: self.center.x - 10, y: self.center.y))
animation.toValue = NSValue(cgPoint: CGPoint(x: self.center.x + 10, y: self.center.y))
self.layer.add(animation, forKey: "position")
}
}
To use
self.vwOffer.shake()
Swift 5
Safe (non crash) shake extension for Corey Pett answer:
extension UIView {
func shake(for duration: TimeInterval = 0.5, withTranslation translation: CGFloat = 10) {
let propertyAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 0.3) {
self.transform = CGAffineTransform(translationX: translation, y: 0)
}
propertyAnimator.addAnimations({
self.transform = CGAffineTransform(translationX: 0, y: 0)
}, delayFactor: 0.2)
propertyAnimator.startAnimation()
}
}
extension CALayer {
func shake(duration: NSTimeInterval = NSTimeInterval(0.5)) {
let animationKey = "shake"
removeAnimationForKey(animationKey)
let kAnimation = CAKeyframeAnimation(keyPath: "transform.translation.x")
kAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
kAnimation.duration = duration
var needOffset = CGRectGetWidth(frame) * 0.15,
values = [CGFloat]()
let minOffset = needOffset * 0.1
repeat {
values.append(-needOffset)
values.append(needOffset)
needOffset *= 0.5
} while needOffset > minOffset
values.append(0)
kAnimation.values = values
addAnimation(kAnimation, forKey: animationKey)
}
}
How to use:
[UIView, UILabel, UITextField, UIButton & etc].layer.shake(NSTimeInterval(0.7))
I tried some of the available solutions but none of them were handling the full shake animation: moving from left to right and get back to the original position.
So, after some investigation I found the right solution that I consider to be a successful shake using UIViewPropertyAnimator.
func shake(completion: (() -> Void)? = nil) {
let speed = 0.75
let time = 1.0 * speed - 0.15
let timeFactor = CGFloat(time / 4)
let animationDelays = [timeFactor, timeFactor * 2, timeFactor * 3]
let shakeAnimator = UIViewPropertyAnimator(duration: time, dampingRatio: 0.3)
// left, right, left, center
shakeAnimator.addAnimations({
self.transform = CGAffineTransform(translationX: 20, y: 0)
})
shakeAnimator.addAnimations({
self.transform = CGAffineTransform(translationX: -20, y: 0)
}, delayFactor: animationDelays[0])
shakeAnimator.addAnimations({
self.transform = CGAffineTransform(translationX: 20, y: 0)
}, delayFactor: animationDelays[1])
shakeAnimator.addAnimations({
self.transform = CGAffineTransform(translationX: 0, y: 0)
}, delayFactor: animationDelays[2])
shakeAnimator.startAnimation()
shakeAnimator.addCompletion { _ in
completion?()
}
shakeAnimator.startAnimation()
}
Final result:
func shakeTextField(textField: UITextField)
{
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.07
animation.repeatCount = 3
animation.autoreverses = true
animation.fromValue = NSValue(cgPoint: CGPoint(x: textField.center.x - 10, y: textField.center.y))
animation.toValue = NSValue(cgPoint: CGPoint(x: textField.center.x + 10, y: textField.center.y))
textField.layer.add(animation, forKey: "position")
textField.attributedPlaceholder = NSAttributedString(string: textField.placeholder ?? "",
attributes: [NSAttributedStringKey.foregroundColor: UIColor.red])
}
//write in base class or any view controller and use it
This is based on CABasicAnimation, it contain also an audio effect :
extension UIView{
var audioPlayer = AVAudioPlayer()
func vibrate(){
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 5
animation.autoreverses = true
animation.fromValue = NSValue(CGPoint: CGPointMake(self.center.x - 5.0, self.center.y))
animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 5.0, self.center.y))
self.layer.addAnimation(animation, forKey: "position")
// audio part
do {
audioPlayer = try AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource(mySoundFileName, ofType: "mp3")!))
audioPlayer.prepareToPlay()
audioPlayer.play()
} catch {
print("∙ Error playing vibrate sound..")
}
}
}
func addShakeAnimation(duration: CGFloat = 0.3, repeatCount: Float = 4, angle: Float = Float.pi / 27, completion: (() -> Void)? = nil) {
let rotationAnimation = CABasicAnimation.init(keyPath: "transform.rotation.z")
rotationAnimation.duration = TimeInterval(duration/CGFloat(repeatCount))
rotationAnimation.repeatCount = repeatCount
rotationAnimation.autoreverses = true
rotationAnimation.fromValue = -angle
rotationAnimation.toValue = angle
rotationAnimation.isRemovedOnCompletion = true
CATransaction.begin()
CATransaction.setCompletionBlock {
if let completion = completion {
completion()
}
}
layer.add(rotationAnimation, forKey: "shakeAnimation")
CATransaction.commit()
}

How to make UIButton shake on tap? [duplicate]

This question already has answers here:
How do you make a shake animation for a button using Swift 3 [closed]
(2 answers)
Closed 5 years ago.
I'm learning about UIView animations using keyframes and spring animations and I'm trying to make a button shake after tapping it. The issue is that I drag and dropped the button from the library and pinned the trailing edge to a UILabel above it and nothing else. In the various examples I see a header constraint but my button has no header. This is the code I have so far
#IBAction func noButtonPressed(_ sender: UIButton) {
UIView.animate(withDuration: 1, delay: 1, usingSpringWithDamping: 0.5, initialSpringVelocity: 15, options: [], animations: {
self.noButtonTrailing.constant = 16
self.view.layoutIfNeeded()
})
}
Am I suppose to make a header constraint somewhere? Thanks
Here is simple media timing animation for linear movement & UIView damping animation.
Note: Swift 4
extension UIView {
// Using CAMediaTimingFunction
func shake(duration: TimeInterval = 0.5, values: [CGFloat]) {
let animation = CAKeyframeAnimation(keyPath: "transform.translation.x")
// Swift 4.2 and above
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
// Swift 4.1 and below
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.duration = duration // You can set fix duration
animation.values = values // You can set fix values here also
self.layer.add(animation, forKey: "shake")
}
// Using SpringWithDamping
func shake(duration: TimeInterval = 0.5, xValue: CGFloat = 12, yValue: CGFloat = 0) {
self.transform = CGAffineTransform(translationX: xValue, y: yValue)
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.4, initialSpringVelocity: 1.0, options: .curveEaseInOut, animations: {
self.transform = CGAffineTransform.identity
}, completion: nil)
}
// Using CABasicAnimation
func shake(duration: TimeInterval = 0.05, shakeCount: Float = 6, xValue: CGFloat = 12, yValue: CGFloat = 0){
let animation = CABasicAnimation(keyPath: "position")
animation.duration = duration
animation.repeatCount = shakeCount
animation.autoreverses = true
animation.fromValue = NSValue(cgPoint: CGPoint(x: self.center.x - xValue, y: self.center.y - yValue))
animation.toValue = NSValue(cgPoint: CGPoint(x: self.center.x + xValue, y: self.center.y - yValue))
self.layer.add(animation, forKey: "shake")
}
}
Button Action
#IBAction func noButtonPressed(button: UIButton) {
// for spring damping animation
//button.shake()
// for CAMediaTimingFunction
button.shake(duration: 0.5, values: [-12.0, 12.0, -12.0, 12.0, -6.0, 6.0, -3.0, 3.0, 0.0])
// for CABasicAnimation
//button.shake(shakeCount: 10)
}
Here is the output for #Krunal code. I used 3 different cases and output is pretty nice :)
#IBAction func instagramButtonClicked(_ sender: Any) {
guard let button = sender as? UIButton else {
return
}
button.shake(duration: 0.5, values: [-12.0, 12.0, -12.0, 12.0, -6.0, 6.0, -3.0, 3.0, 0.0])
}
#IBAction func facebookButtonClicked(_ sender: Any) {
guard let button = sender as? UIButton else {
return
}
button.shake()
}
#IBAction func websiteButtonClicked(_ sender: Any) {
guard let button = sender as? UIButton else {
return
}
button.shake(duration: 0.5, values: [-1.0, 1.0, -6.0, 6.0, -10.0, 10.0, -12.0, 12.0])
}
I think you are looking for this:
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.07
animation.repeatCount = 4
animation.autoreverses = true
animation.fromValue = NSValue(cgPoint: CGPoint(x: self.BUTTON.center.x - 10, y: BUTTON.center.y))
animation.toValue = NSValue(cgPoint: CGPoint(x: BUTTON.center.x + 10, y: BUTTON.center.y))
BUTTON.layer.add(animation, forKey: "position")

Shake Animation for UITextField/UIView in Swift

I am trying to figure out how to make the text Field shake on button press when the user leaves the text field blank.
I currently have the following code working:
if self.subTotalAmountData.text == "" {
let alertController = UIAlertController(title: "Title", message:
"What is the Sub-Total!", preferredStyle: UIAlertControllerStyle.Alert)
alertController.addAction(UIAlertAction(title: "Okay", style: UIAlertActionStyle.Default,handler: nil))
self.presentViewController(alertController, animated: true, completion: nil)
} else {
}
But i think it would be much more appealing to just have the text field shake as an alert.
I can't find anything to animate the text field.
Any ideas?
Thanks!
You can change the duration and repeatCount and tweak it. This is what I use in my code. Varying the fromValue and toValue will vary the distance moved in the shake.
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.07
animation.repeatCount = 4
animation.autoreverses = true
animation.fromValue = NSValue(cgPoint: CGPoint(x: viewToShake.center.x - 10, y: viewToShake.center.y))
animation.toValue = NSValue(cgPoint: CGPoint(x: viewToShake.center.x + 10, y: viewToShake.center.y))
viewToShake.layer.add(animation, forKey: "position")
The following function is used in any view.
extension UIView {
func shake() {
let animation = CAKeyframeAnimation(keyPath: "transform.translation.x")
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.duration = 0.6
animation.values = [-20.0, 20.0, -20.0, 20.0, -10.0, 10.0, -5.0, 5.0, 0.0 ]
layer.add(animation, forKey: "shake")
}
}
EDIT: using CABasicAnimation cause the app to crash if you ever trigger the animation twice in a row. So be sure to use CAKeyframeAnimation. Bug has been fixed, thanks to the comments :)
Or you can use this if you want more parameters (in swift 5) :
public extension UIView {
func shake(count : Float = 4,for duration : TimeInterval = 0.5,withTranslation translation : Float = 5) {
let animation = CAKeyframeAnimation(keyPath: "transform.translation.x")
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.repeatCount = count
animation.duration = duration/TimeInterval(animation.repeatCount)
animation.autoreverses = true
animation.values = [translation, -translation]
layer.add(animation, forKey: "shake")
}
}
You can call this function on any UIView, UIButton, UILabel, UITextView etc. This way
yourView.shake()
Or this way if you want to add some custom parameters to the animation:
yourView.shake(count: 5, for: 1.5, withTranslation: 10)
I think all of these are dangerous.
If your shake animation is based on a user action and that user action is triggered while animating.
CRAAAAAASH
Here is my way in Swift 4:
static func shake(view: UIView, for duration: TimeInterval = 0.5, withTranslation translation: CGFloat = 10) {
let propertyAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 0.3) {
view.transform = CGAffineTransform(translationX: translation, y: 0)
}
propertyAnimator.addAnimations({
view.transform = CGAffineTransform(translationX: 0, y: 0)
}, delayFactor: 0.2)
propertyAnimator.startAnimation()
}
Maybe not the cleanest, but this method can be triggered repeatedly and is easily understood
Edit:
I am a huge proponent for usage of UIViewPropertyAnimator. So many cool features that allow you to make dynamic modifications to basic animations.
Here is another example to add a red border while the view is shaking, then removing it when the shake finishes.
static func shake(view: UIView, for duration: TimeInterval = 0.5, withTranslation translation: CGFloat = 10) {
let propertyAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 0.3) {
view.layer.borderColor = UIColor.red.cgColor
view.layer.borderWidth = 1
view.transform = CGAffineTransform(translationX: translation, y: 0)
}
propertyAnimator.addAnimations({
view.transform = CGAffineTransform(translationX: 0, y: 0)
}, delayFactor: 0.2)
propertyAnimator.addCompletion { (_) in
view.layer.borderWidth = 0
}
propertyAnimator.startAnimation()
}
Swift 5.0
extension UIView {
func shake(){
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.07
animation.repeatCount = 3
animation.autoreverses = true
animation.fromValue = NSValue(cgPoint: CGPoint(x: self.center.x - 10, y: self.center.y))
animation.toValue = NSValue(cgPoint: CGPoint(x: self.center.x + 10, y: self.center.y))
self.layer.add(animation, forKey: "position")
}
}
To use
self.vwOffer.shake()
Swift 5
Safe (non crash) shake extension for Corey Pett answer:
extension UIView {
func shake(for duration: TimeInterval = 0.5, withTranslation translation: CGFloat = 10) {
let propertyAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 0.3) {
self.transform = CGAffineTransform(translationX: translation, y: 0)
}
propertyAnimator.addAnimations({
self.transform = CGAffineTransform(translationX: 0, y: 0)
}, delayFactor: 0.2)
propertyAnimator.startAnimation()
}
}
extension CALayer {
func shake(duration: NSTimeInterval = NSTimeInterval(0.5)) {
let animationKey = "shake"
removeAnimationForKey(animationKey)
let kAnimation = CAKeyframeAnimation(keyPath: "transform.translation.x")
kAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
kAnimation.duration = duration
var needOffset = CGRectGetWidth(frame) * 0.15,
values = [CGFloat]()
let minOffset = needOffset * 0.1
repeat {
values.append(-needOffset)
values.append(needOffset)
needOffset *= 0.5
} while needOffset > minOffset
values.append(0)
kAnimation.values = values
addAnimation(kAnimation, forKey: animationKey)
}
}
How to use:
[UIView, UILabel, UITextField, UIButton & etc].layer.shake(NSTimeInterval(0.7))
I tried some of the available solutions but none of them were handling the full shake animation: moving from left to right and get back to the original position.
So, after some investigation I found the right solution that I consider to be a successful shake using UIViewPropertyAnimator.
func shake(completion: (() -> Void)? = nil) {
let speed = 0.75
let time = 1.0 * speed - 0.15
let timeFactor = CGFloat(time / 4)
let animationDelays = [timeFactor, timeFactor * 2, timeFactor * 3]
let shakeAnimator = UIViewPropertyAnimator(duration: time, dampingRatio: 0.3)
// left, right, left, center
shakeAnimator.addAnimations({
self.transform = CGAffineTransform(translationX: 20, y: 0)
})
shakeAnimator.addAnimations({
self.transform = CGAffineTransform(translationX: -20, y: 0)
}, delayFactor: animationDelays[0])
shakeAnimator.addAnimations({
self.transform = CGAffineTransform(translationX: 20, y: 0)
}, delayFactor: animationDelays[1])
shakeAnimator.addAnimations({
self.transform = CGAffineTransform(translationX: 0, y: 0)
}, delayFactor: animationDelays[2])
shakeAnimator.startAnimation()
shakeAnimator.addCompletion { _ in
completion?()
}
shakeAnimator.startAnimation()
}
Final result:
func shakeTextField(textField: UITextField)
{
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.07
animation.repeatCount = 3
animation.autoreverses = true
animation.fromValue = NSValue(cgPoint: CGPoint(x: textField.center.x - 10, y: textField.center.y))
animation.toValue = NSValue(cgPoint: CGPoint(x: textField.center.x + 10, y: textField.center.y))
textField.layer.add(animation, forKey: "position")
textField.attributedPlaceholder = NSAttributedString(string: textField.placeholder ?? "",
attributes: [NSAttributedStringKey.foregroundColor: UIColor.red])
}
//write in base class or any view controller and use it
This is based on CABasicAnimation, it contain also an audio effect :
extension UIView{
var audioPlayer = AVAudioPlayer()
func vibrate(){
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 5
animation.autoreverses = true
animation.fromValue = NSValue(CGPoint: CGPointMake(self.center.x - 5.0, self.center.y))
animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 5.0, self.center.y))
self.layer.addAnimation(animation, forKey: "position")
// audio part
do {
audioPlayer = try AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource(mySoundFileName, ofType: "mp3")!))
audioPlayer.prepareToPlay()
audioPlayer.play()
} catch {
print("∙ Error playing vibrate sound..")
}
}
}
func addShakeAnimation(duration: CGFloat = 0.3, repeatCount: Float = 4, angle: Float = Float.pi / 27, completion: (() -> Void)? = nil) {
let rotationAnimation = CABasicAnimation.init(keyPath: "transform.rotation.z")
rotationAnimation.duration = TimeInterval(duration/CGFloat(repeatCount))
rotationAnimation.repeatCount = repeatCount
rotationAnimation.autoreverses = true
rotationAnimation.fromValue = -angle
rotationAnimation.toValue = angle
rotationAnimation.isRemovedOnCompletion = true
CATransaction.begin()
CATransaction.setCompletionBlock {
if let completion = completion {
completion()
}
}
layer.add(rotationAnimation, forKey: "shakeAnimation")
CATransaction.commit()
}

Resources