How to make UIButton shake on tap? [duplicate] - ios

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")

Related

Add shimmer animation flowing from one view to another

I am trying to create shimmer animation flowing from one view to another view. Below is the code that I have so far:
override func viewDidLoad() {
super.viewDidLoad()
view1.backgroundColor = .purple
view2.backgroundColor = .purple
view1.startAnimating()
//let seconds = 2.0
//DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
self.view2.startAnimating()
//}
}
class ShimmerView: UIView {
let gradientColorOne : CGColor = UIColor.purple.cgColor
let gradientColorTwo : CGColor = UIColor.yellow.cgColor
func addGradientLayer() -> CAGradientLayer {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.bounds
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0)
gradientLayer.colors = [gradientColorOne, gradientColorTwo, gradientColorOne]
gradientLayer.locations = [0.0, 0.5, 1.0]
self.layer.addSublayer(gradientLayer)
return gradientLayer
}
func addAnimation() -> CABasicAnimation {
let animation = CABasicAnimation(keyPath: "locations")
animation.fromValue = [-1.0, -0.5, 0.0]
animation.toValue = [1.0, 1.5, 2.0]
animation.repeatCount = .infinity
animation.duration = 2
return animation
}
func startAnimating() {
let gradientLayer = addGradientLayer()
let animation = addAnimation()
gradientLayer.add(animation, forKey: animation.keyPath)
}
}
I tried to add delay but I guess that's not going to work because next time the animation should start with further delay.
This is how it behaves right now. Animation happens in parallel:
But, what I want is that it to start the animation from view1 and then finishes in view2 and it keep on doing that. Any ideas/ suggestions on how to achieve this? Thank you.
You can try controlling the animation sequencing from call site like following.
Define a constant called animationDuration inside the ShimmerView like this.
public static let animationDuration: Double = 2.0
Update the addAnimation() implementation like this.
// Remove this line, We need this to be performed only once at a time.
/*
animation.repeatCount = .infinity
*/
// Update this line to use the constant defined earlier
animation.duration = ShimmerView.animationDuration
From your call site, you can now do this.
class ViewController: UIViewController {
let view1 = ShimmerView()
let view2 = ShimmerView()
func startAnimating() {
view1.startAnimating()
let interval = ShimmerView.animationDuration
DispatchQueue.main.asyncAfter(deadline: .now() + interval) { [weak self] in
self?.view2.startAnimating()
DispatchQueue.main.asyncAfter(deadline: .now() + interval) { [weak self] in
self?.startAnimating()
}
}
}
}
UPDATE
This kind of almost works, but when view1 finishes the animation and view2 starts it, the gradient on view1 sits in the middle and vice versa. So, it doesn't give a clear flowing animation from top to bottom that goes on.
The problem is your code is not removing the gradientLayer instance from your view upon animation completion. You can do it with CATransaction.setCompletionBlock like following.
import Foundation
import UIKit
class ShimmerView: UIView {
static let animationDuration: Double = 2.0
let gradientColorOne: CGColor = UIColor.purple.cgColor
let gradientColorTwo: CGColor = UIColor.yellow.cgColor
lazy var gradientLayer: CAGradientLayer = {
let gradient = CAGradientLayer()
gradient.startPoint = CGPoint(x: 0.0, y: 0.0)
gradient.endPoint = CGPoint(x: 0.0, y: 1.0)
gradient.colors = [gradientColorOne, gradientColorTwo, gradientColorOne]
gradient.locations = [0.0, 0.5, 1.0]
return gradient
}()
func addGradientLayer() {
gradientLayer.frame = self.bounds
self.layer.addSublayer(gradientLayer)
}
func removeGradientLayer() {
gradientLayer.removeFromSuperlayer()
}
func addAnimation() -> CABasicAnimation {
let animation = CABasicAnimation(keyPath: "locations")
animation.fromValue = [-1.0, -0.5, 0.0]
animation.toValue = [1.0, 1.5, 2.0]
animation.duration = ShimmerView.animationDuration
return animation
}
func startAnimating() {
self.addGradientLayer()
let animation = addAnimation()
CATransaction.begin()
// Setting this block before adding the animation is crucial
CATransaction.setCompletionBlock({ [weak self] in
self?.removeGradientLayer()
})
gradientLayer.add(animation, forKey: animation.keyPath)
CATransaction.commit()
}
}

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()
}

Rotate UIButton 360 degrees

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)
}

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