71

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!

rakeshbs
  • 24,392
  • 7
  • 73
  • 63
Joe
  • 827
  • 2
  • 9
  • 13
  • What have you done to try to animate the text field? – David Berry Jan 16 '15 at 16:21
  • I have nop idea where to start. nothing makes sense to me, and I cant find any tutorials that show even the slightest variation that I can adopt. I have tried animateWithDuration UIViewAnimationOptions.Transition but the code alwasy error out how i make sense of it. – Joe Jan 16 '15 at 16:46
  • 1
    Google for iPhone animation tutorials. Ray Wanderlich's are usually very good and readable. – David Berry Jan 16 '15 at 16:48
  • Thanks I will read up and understand the best I can! – Joe Jan 16 '15 at 17:05

11 Answers11

169

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")
Vasily
  • 3,740
  • 3
  • 27
  • 61
rakeshbs
  • 24,392
  • 7
  • 73
  • 63
  • 1
    Not sure why but i am getting this exception: `2015-05-17 19:43:00.744 pdfdistributor[19908:2303862] -[pdfdistributor.ViewController mLoginBT:]: unrecognized selector sent to instance 0x7fca90d17e00 2015-05-17 19:43:00.749 pdfdistributor[19908:2303862] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[pdfdistributor.ViewController mLoginBT:]: unrecognized selector sent to instance 0x7fca90d17e00' *** First throw call stack:` – Shajeel Afzal May 17 '15 at 14:44
  • 1
    Swift 3 version: let animation = CABasicAnimation(keyPath: "position") animation.duration = 0.07 animation.repeatCount = 4 animation.autoreverses = true animation.fromValue = NSValue(cgPoint: CGPoint(x: txtField.center.x - 10, y: txtField.center.y)) animation.toValue = NSValue(cgPoint: CGPoint(x: txtField.center.x + 10, y: txtField.center.y)) txtField.layer.add(animation, forKey: "position") – malex Aug 04 '17 at 20:52
  • My view moves a few pixels up on the y-axis and then the view animates. What seems to be the problem? – Harsh Mehta May 16 '19 at 05:12
155

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")
    }
}
Sahil Kapoor
  • 11,183
  • 13
  • 64
  • 87
Surckarter
  • 1,660
  • 1
  • 10
  • 7
42

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)
RomOne
  • 2,065
  • 17
  • 29
  • 1
    perfect solution. for a simple shake I use `sender.shake(count: 3, for: 0.3, withTanslation: 10)` – DanielZanchi Oct 28 '16 at 15:19
  • 2
    This solution is incorrect. The shake animation only animates to one side. I.e. [-5 to 0]. The correct solution would be [-2.5 to +2.5]. This incorrect solution means that the shake doesn't happen from the center of the view, and just visually looks off. – ChrisJF Mar 22 '18 at 01:11
  • 2
    This actually crashes if you try to tap on the uitextfield thats animating. – Sanket Ray Sep 16 '18 at 13:49
  • DO NOT use this, it will crash with VERY CRYPTIC error messages if you perform another animation simultaneously. You might crashes mentioning "[NSConcreteValue doubleValue]" unrecognized selector sent to instance Fatal Exception: NSInvalidArgumentException and `CoreFoundation -[NSOrderedSet initWithSet:copyItems:]` ) – Sajjon Apr 13 '19 at 15:27
28

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()
}
Corey Pett
  • 628
  • 8
  • 8
  • well this doesn't crash at least when we try to tap on the uitextfield while its animating. I wanna add a red border color while its animating and remove that after animation. How do you propose to do that? – Sanket Ray Sep 16 '18 at 14:04
  • great reminder about more convenient API than CA set! +1 – dimpiax Feb 17 '22 at 10:35
17

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()
Hardik Thakkar
  • 15,269
  • 2
  • 94
  • 81
8

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()
    }
}
Vahid
  • 3,352
  • 2
  • 34
  • 42
5
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))
5

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:

shake-animation-result

ricardopereira
  • 11,118
  • 5
  • 63
  • 81
2
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

1

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..")
        }
    }
}
Alessandro Ornano
  • 34,887
  • 11
  • 106
  • 133
1
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()
}
Serg Smyk
  • 613
  • 5
  • 11