52

I want to wobble an image back and forth in my application similar to how the iPhone icons wobble when you press down on it. What's the best way to do that?

This is my first foray into animations that's not using an animated GIF. I think the idea is to slightly rotate the image back and forth to create the wobbling effect. I've looked at using CABasicAnimation and CAKeyframeAnimation. CABasicAnimation creates a jitter every time it repeats because it jumps to the from position and doesn't interpolate back. CAKeyframeAnimation seems like the solution except that I can't get it to work. I must be missing something. Here's my code using the CAKeyframeAnimation (which doesn't work):

    NSString *keypath = @"wobbleImage";
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:keypath];
animation.duration = 1.0f;
animation.delegate = self;
animation.repeatCount = 5;

CGFloat wobbleAngle = 0.0872664626f;
NSValue *initial = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(0.0f, 0.0f, 0.0f, 1.0f)];
NSValue *middle = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(wobbleAngle, 0.0f, 0.0f, 1.0f)];
NSValue *final = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(-wobbleAngle, 0.0f, 0.0f, 1.0f)];
animation.values = [NSArray arrayWithObjects:initial, middle, final, nil];

[imageView.layer addAnimation:animation forKey:keypath];


Or there could be a totally simpler solution that I'm just missing. Appreciate any pointers. Thanks!

Pieter
  • 17,435
  • 8
  • 50
  • 89
jeanniey
  • 523
  • 1
  • 5
  • 5

13 Answers13

92

Ramin's answer was very good, but since OS4 the same effect can be achieved using animateWithDuration in one simple function too.

(adapted his example for future googlers)

#define RADIANS(degrees) (((degrees) * M_PI) / 180.0)

- (void)startWobble {
 itemView.transform = CGAffineTransformRotate(CGAffineTransformIdentity, RADIANS(-5));

 [UIView animateWithDuration:0.25 
      delay:0.0 
      options:(UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionRepeat | UIViewAnimationOptionAutoreverse)
      animations:^ {
       itemView.transform = CGAffineTransformRotate(CGAffineTransformIdentity, RADIANS(5));
      }
      completion:NULL
 ];
}

- (void)stopWobble {
 [UIView animateWithDuration:0.25
      delay:0.0 
      options:(UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveLinear)
      animations:^ {
       itemView.transform = CGAffineTransformIdentity;
      }
      completion:NULL
  ];
}
meaning-matters
  • 21,929
  • 10
  • 82
  • 142
Pieter
  • 17,435
  • 8
  • 50
  • 89
92

Simple way to do it:

#define RADIANS(degrees) (((degrees) * M_PI) / 180.0)

CGAffineTransform leftWobble = CGAffineTransformRotate(CGAffineTransformIdentity, RADIANS(-5.0));
CGAffineTransform rightWobble = CGAffineTransformRotate(CGAffineTransformIdentity, RADIANS(5.0));

itemView.transform = leftWobble;  // starting point

[UIView beginAnimations:@"wobble" context:itemView];
[UIView setAnimationRepeatAutoreverses:YES]; // important
[UIView setAnimationRepeatCount:10];
[UIView setAnimationDuration:0.25];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:@selector(wobbleEnded:finished:context:)];

itemView.transform = rightWobble; // end here & auto-reverse

[UIView commitAnimations];

...

- (void) wobbleEnded:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context 
{
     if ([finished boolValue]) {
        UIView* item = (UIView *)context;
        item.transform = CGAffineTransformIdentity;
     }
}

Probably have to play with timing and angles but this should get you started.

EDIT: I edited the response to add code to put the item back in its original state when done. Also, note that you can use the beginAnimations context value to pass along anything to the start/stop methods. In this case it's the wobbling object itself so you don't have to rely on specific ivars and the method can be used for any generic UIView-based object (i.e. text labels, images, etc.)

meaning-matters
  • 21,929
  • 10
  • 82
  • 142
Ramin
  • 13,343
  • 3
  • 33
  • 35
  • this works very well, but the image ends up in a rotated state when i want it to end back upright in the original state. do you know how that can be done in the animation block itself? must i use the animationDidStopSelector to reset position? – jeanniey May 30 '09 at 17:32
  • Nice! I haven't tried it, but it looks like if the view would abrupt start with an rotated transform, and abrupt end from an rotated transform to the identity transform (= no rotation). So you'll need some more code here to get it right, i.e. make 3 phases. phase 1) rotate animated to leftWobble, phase 2) do that animation stuff, phase 3) rotate animated back to identity transform. – Thanks May 30 '09 at 22:05
  • Thanks! This works great for what I need. If going for a smoother wobble, the 3 phases look like it would work, although would it make sense to use the CAKeyframeAnimation then? – jeanniey May 31 '09 at 05:34
  • Yes. For more complex multiphase animations--especially if there are timing constraints--core animation is easier to manage and maintain than a chain of UIView animations. UIView is better for 'set and forget' type operations especially when it comes to simple movement, scaling, and rotation. It's also handy for changing view properties over time, like alpha and color values. – Ramin May 31 '09 at 17:55
  • This was very helpful. Question: Is there a way to have this animation run infinitely UNTIL the user taps on the view? – wgpubs Apr 13 '10 at 08:26
  • You can set the RepeatCount to a very high number to have it run for a long time. To get a user tap, override the 'touchesEnded' method on the UIView and stop the animation. – Ramin May 04 '10 at 18:20
  • Wow :) (awesome) ossam (not appended a :) ) – sagarkothari Jul 10 '10 at 05:30
  • It would be nice if the button could wobble left and then right, so it rocks from side to side rather than just to the left... would this be easy? – Smikey Feb 17 '12 at 12:53
14

You should use CAKeyframeAnimation to make a smoother animation.

+ (void) animationKeyFramed: (CALayer *) layer 
                   delegate: (id) object
              forKey: (NSString *) key {

    CAKeyframeAnimation *animation;
    animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation.z"];
    animation.duration = 0.4;
    animation.cumulative = YES;
    animation.repeatCount = 2;
    animation.values = [NSArray arrayWithObjects:
            [NSNumber numberWithFloat: 0.0], 
            [NSNumber numberWithFloat: RADIANS(-9.0)], 
            [NSNumber numberWithFloat: 0.0],
            [NSNumber numberWithFloat: RADIANS(9.0)],
            [NSNumber numberWithFloat: 0.0], nil];
    animation.fillMode = kCAFillModeForwards;
    animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
    animation.removedOnCompletion = NO;
    animation.delegate = object;

    [layer addAnimation:animation forKey:key];
}
docchang
  • 1,115
  • 15
  • 32
9

I have written a sample app that attempts to replicate the home screen wobble and icon movement: iPhone Sample Code: Tiles

Kristopher Johnson
  • 81,409
  • 55
  • 245
  • 302
  • 3
    I appreciate that this code sample also includes a translation in the Y direction; it's a subtle effect that was missing from others' solutions. – Tim Arnold Oct 03 '12 at 16:29
4

For anyone who has come across this posting more recently and would like to do the same in Swift, here is my translation:

func smoothJiggle() {

    let degrees: CGFloat = 5.0
    let animation = CAKeyframeAnimation(keyPath: "transform.rotation.z")
    animation.duration = 0.6
    animation.cumulative = true
    animation.repeatCount = Float.infinity
    animation.values = [0.0,
        degreesToRadians(-degrees) * 0.25,
        0.0,
        degreesToRadians(degrees) * 0.5,
        0.0,
        degreesToRadians(-degrees),
        0.0,
        degreesToRadians(degrees),
        0.0,
        degreesToRadians(-degrees) * 0.5,
        0.0,
        degreesToRadians(degrees) * 0.25,
        0.0]
    animation.fillMode = kCAFillModeForwards;
    animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
    animation.removedOnCompletion = true

    layer.addAnimation(animation, forKey: "wobble")
}

func stopJiggling() {
    jiggling = false
    self.layer.removeAllAnimations()
    self.transform = CGAffineTransformIdentity
    self.layer.anchorPoint = CGPointMake(0.5, 0.5)
}
Allen Conquest
  • 126
  • 1
  • 3
2

You can create a not-endless wobble effect using the CAKeyframeAnimation, like so:

CGFloat degrees = 8.0;
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation.z"];
animation.duration = 0.6;
animation.cumulative = YES;
animation.repeatCount = 1;
animation.values = @[@0.0,
                    @RADIANS(-degrees) * 0.25,
                    @0.0,
                    @RADIANS(degrees) * 0.5,
                    @0.0,
                    @RADIANS(-degrees),
                    @0.0,
                    @RADIANS(degrees),
                    @0.0,
                    @RADIANS(-degrees) * 0.5,
                    @0.0,
                    @RADIANS(degrees) * 0.25,
                    @0.0];
animation.fillMode = kCAFillModeForwards;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
animation.removedOnCompletion = YES;

[self.layer addAnimation:animation forKey:@"wobble"];
Berik
  • 7,816
  • 2
  • 32
  • 40
2

from above answers, I got the swift5 version:

  func startWobble() {
    let angle = 5.0 * Double.pi / 180.0;
    self.transform = CGAffineTransform.identity.rotated(by: CGFloat(-angle));
    UIView.animate(withDuration: 0.25, delay: 0, options: [.allowUserInteraction,.repeat,.autoreverse], animations: {
         self.transform = CGAffineTransform.identity.rotated(by: CGFloat(angle));
    }, completion: nil)

}

func stopWobble() {

    UIView.animate(withDuration: 0.25, delay: 0, options: [.allowUserInteraction,.beginFromCurrentState,.curveLinear], animations: {
        self.transform = CGAffineTransform.identity;
    }, completion: nil)
}
Jagie
  • 2,190
  • 3
  • 27
  • 25
2

The easiest way I know is to use Core Animation. Basically, you create an Core Animation Block, then do an rotation transform and setup and repeat count. Core Animation then takes care of everything that's needed to do this wobbling effect.

To start an Core Animation block, just do:

[UIView beginAnimations:@"any string as animationID" context:self];
[UIView setAnimationRepeatCount:10];
// rotate 
[UIView commitAnimations];

not tested. But it can be that you will also have to do:

[UIView setAnimationBeginsFromCurrentState:YES];

A.F.A.I.K. setAnimationRepeatCount will have the effect that the animation gets done, undone, done, undone, done, undone, done... as many times as you specify. So you may want to first rotate to left with no repeat count, and then from this point start wobbling with repeat count. When done, you may want to rotate back to the identity transform (= no rotation and scaling applied).

You can chain animations by setting the animation delegate with

[UIView setAnimationDelegate:self]

and then

[UIView setAnimationDidStopSelector:@selector(myMethod:finished:context:)];

and as soon as the animation stops, that method will be called. See the UIView class documentation for how to implement that method that will be called when the animation stops. Basically, inside that method you would perform the next step (i.e. rotating back, or anything else), with an new animation block but same context and animation ID, and then (if needed) specify another didStopSelector.

UPDATE:

You may want to check out:

[UIView setAnimationRepeatAutoreverses:YES];

this will wobble back and forth automatically.

Thanks
  • 40,109
  • 71
  • 208
  • 322
  • thanks Thanks! can i just setup a chain of animation blocks one after the other in my function without using the DidStopSelector? are there any best practices or performance considerations? – jeanniey May 30 '09 at 17:36
  • No. I think you need to use the DidStopSelector. Unfortunately, that looks a little unhandy in code, since you will have to create a method for every anymation phase. But you can re-use them if your animation consists of simple phases like "wobble to left", "wobble to center" and "wobble to right". If you don't like to have more than one method for that, you could specify the method itself as the didStopSelector. But that would make things more complicated. I typically set up a series of methods one after another to chain them. And when you're done, set the didStopSelector to nil. – Thanks May 30 '09 at 18:35
  • Updated: See [UIView setAnimationRepeatAutoreverses:YES]; – Thanks May 30 '09 at 22:59
1

Late to the party. I'm typically using spring with damping (iOS7 and newer). In swift it looks like:

    sender.transform = CGAffineTransformMakeScale(1.2, 1.2)
    UIView.animateWithDuration(0.30, delay: 0.0, usingSpringWithDamping: 0.3, initialSpringVelocity: 0.3, options: UIViewAnimationOptions.CurveEaseInOut, animations: { () -> Void in
        sender.transform = CGAffineTransformMakeScale(1, 1)
    }) { (Bool) -> Void in
    //Do stuff when animation is finished
    }

You can tweak the effect by adjusting the initialSpringVelocity and SpringWithDamping

EsbenB
  • 3,356
  • 25
  • 44
1

Based on EsbenB's answer, but updated for Swift 3 and for rotation:

    sender.transform = CGAffineTransform(rotationAngle: 12.0 * .pi / 180.0)
    UIView.animate(withDuration: 0.60, delay: 0.0, usingSpringWithDamping: 0.3, initialSpringVelocity: 0.3, options: .curveEaseInOut, animations: { () -> Void in
        sender.transform = CGAffineTransform(rotationAngle: 0.0)
    }, completion: nil)
user599849
  • 21
  • 1
0

Well the code given by Ramin works well.But if you use tabbar application and move to next tab item then again come back to previous tab item,you will see that your view has been moved to left,every time.So the best practice is that you use ViewWillAppear method as.

- (void)viewWillAppear:(BOOL)animated
{
    UIView* item = self.view;
    item.transform = CGAffineTransformIdentity;
}

so the every time view is loaded you will find you animation at the right place.And also use this method as well.

[UIView setAnimationDidStopSelector:@selector(myMethod:finished:context:)];
Community
  • 1
  • 1
Sabby
  • 2,586
  • 2
  • 27
  • 41
0

Swift 3

func startWiggling() {
    deleteButton.isHidden = false
    guard contentView.layer.animation(forKey: "wiggle") == nil else { return }
    guard contentView.layer.animation(forKey: "bounce") == nil else { return }

    let angle = 0.04

    let wiggle = CAKeyframeAnimation(keyPath: "transform.rotation.z")
    wiggle.values = [-angle, angle]
    wiggle.autoreverses = true
    wiggle.duration = randomInterval(0.1, variance: 0.025)
    wiggle.repeatCount = Float.infinity
    contentView.layer.add(wiggle, forKey: "wiggle")

    let bounce = CAKeyframeAnimation(keyPath: "transform.translation.y")
    bounce.values = [4.0, 0.0]
    bounce.autoreverses = true
    bounce.duration = randomInterval(0.12, variance: 0.025)
    bounce.repeatCount = Float.infinity
    contentView.layer.add(bounce, forKey: "bounce")
}

func stopWiggling() {
    deleteButton.isHidden = true
    contentView.layer.removeAllAnimations()
}
Beslan Tularov
  • 3,111
  • 1
  • 21
  • 34
0

Easiest way to this with Swift 4+ :

static func wobbleAnimation(button: UIButton) {
    CATransaction.begin()
    let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
    rotateAnimation.autoreverses = true
    rotateAnimation.repeatCount = Float.greatestFiniteMagnitude
    rotateAnimation.fromValue = CGFloat(-0.2)
    rotateAnimation.toValue = CGFloat(0.2)
    rotateAnimation.duration = 0.20
    button.layer.add(rotateAnimation, forKey: nil)
    CATransaction.commit()
}

static func stopWobbleAnimation(button: UIButton) {
    button.layer.removeAllAnimations()
}
Coder ACJHP
  • 1,940
  • 1
  • 20
  • 34