22

I am writing an iPad app that presents user documents similar to the way Pages presents them (as large icons of the actual document). I also want to mimic the jiggling behavior when the user taps the edit button. This is the same jiggle pattern that icons make on the home screen when you tap and hold on them on both the iPhone and iPad.

I've searched the Internet and have found a few algorithms but they just cause the view to rock back and forth which is not at all like the Apple jiggle. It seems there is some randomness in there as each icon jiggles a little differently.

Does anyone have or know of some code that can re-create the same jiggle pattern (or something very close to it)? Thanks!!!

Vic320
  • 1,105
  • 2
  • 10
  • 22

11 Answers11

33


@Vic320's answer is good but personally I don't like the translation. I've edited his code to provide a solution that I personally feel looks more like the springboard wobble effect. Mostly, it's achieved by adding a little randomness and focusing on rotation, without translation:

#define degreesToRadians(x) (M_PI * (x) / 180.0)
#define kAnimationRotateDeg 1.0

- (void)startJiggling {
    NSInteger randomInt = arc4random_uniform(500);
    float r = (randomInt/500.0)+0.5;

    CGAffineTransform leftWobble = CGAffineTransformMakeRotation(degreesToRadians( (kAnimationRotateDeg * -1.0) - r ));
    CGAffineTransform rightWobble = CGAffineTransformMakeRotation(degreesToRadians( kAnimationRotateDeg + r ));

     self.transform = leftWobble;  // starting point

     [[self layer] setAnchorPoint:CGPointMake(0.5, 0.5)];

     [UIView animateWithDuration:0.1
                           delay:0
                         options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionRepeat | UIViewAnimationOptionAutoreverse
                       animations:^{ 
                                 [UIView setAnimationRepeatCount:NSNotFound];
                                 self.transform = rightWobble; }
                      completion:nil];
}
- (void)stopJiggling {
    [self.layer removeAllAnimations];
    self.transform = CGAffineTransformIdentity;
}


Credit where credit's due though, @Vic320's answer provided the basis for this code so +1 for that.

imnk
  • 4,342
  • 3
  • 29
  • 31
  • If your anchor point is set to somewhere else, other than 0.5,0.5 the rotation would make the button "orbit" around the anchor point. This may be a cool effect by itself :) – Alex Stone Nov 24 '11 at 01:47
  • yes Alex! probably not desirable for OP but that would be a cool effect. – imnk Nov 28 '11 at 16:55
  • 1
    Couple more points: [UIView setAnimationRepeatCount:NSNotFound]; is not required, because UIViewAnimationOptionRepeat has been set. And to stop the animation, do this: [self.layer removeAllAnimations]; – sdsykes Jan 13 '12 at 08:33
  • In addition to what @sdsykes said to remove all the animations, you may also need to reset the transform so that your view is not rotated. self.transform = CGAffineTransformIdentity; – DiscDev Sep 09 '13 at 18:45
  • @spotdog13 I mentioned in my answer that "@Vic320's answer provided the basis for this code". My code is meant to replace his -[startJiggling:] method. He provides a -[stopJiggling] method that removes the animations and resets the transform as you guys describe so I didn't feel the need to repeat that here. – imnk Sep 10 '13 at 10:17
  • @imnk - I agree, however if you skip to the most up-voted answer (yours) and read all the comments, you'll see sdsykes comment about calling removeAllAnimations and no mention of reseting the transform. Just adding my comment for others that do the same thing I did. – DiscDev Sep 10 '13 at 20:28
  • For those looking for a solution in Swift this moves over well but I got caught on `options`. In Swift 2 it appears that `options` wants an array of options. This worked for me: `options: [.Autoreverse, .Repeat, .AllowUserInteraction]` – Murray Sagal Dec 17 '15 at 16:42
  • Nice. I found it looks better if the views don't change direction at the same time. Using different delay values can be used to offset the animations, for instance: `static NSTimeInterval delay = 0.0;` then increment it by 0.02 (a fifth) for the next item: `delay = ( delay < 0.07 ) ? delay + 0.02 : 0.00;`. – Rolleric Jan 01 '16 at 20:59
11

OK, so the openspringboard code didn't quite do it for me but I did allow me to create some code that I think is a bit better, still not prefect but better. If anyone has suggestions to make this better, I would love to hear them... (add this to the subclass of the view(s) you want to jiggle)

#define degreesToRadians(x) (M_PI * (x) / 180.0)
#define kAnimationRotateDeg 1.0
#define kAnimationTranslateX 2.0
#define kAnimationTranslateY 2.0

- (void)startJiggling:(NSInteger)count {

    CGAffineTransform leftWobble = CGAffineTransformMakeRotation(degreesToRadians( kAnimationRotateDeg * (count%2 ? +1 : -1 ) ));
    CGAffineTransform rightWobble = CGAffineTransformMakeRotation(degreesToRadians( kAnimationRotateDeg * (count%2 ? -1 : +1 ) ));
    CGAffineTransform moveTransform = CGAffineTransformTranslate(rightWobble, -kAnimationTranslateX, -kAnimationTranslateY);
    CGAffineTransform conCatTransform = CGAffineTransformConcat(rightWobble, moveTransform);

    self.transform = leftWobble;  // starting point

    [UIView animateWithDuration:0.1
                          delay:(count * 0.08)
                        options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionRepeat | UIViewAnimationOptionAutoreverse
                     animations:^{ self.transform = conCatTransform; }
                     completion:nil];
}

- (void)stopJiggling {
    [self.layer removeAllAnimations];
    self.transform = CGAffineTransformIdentity;  // Set it straight 
}
Vic320
  • 1,105
  • 2
  • 10
  • 22
9

@mientus Original Apple Jiggle code in Swift 4, with optional parameters to adjust the duration (i.e. speed), displacement (i.e. position change) and degrees (i.e. rotation amount).

private func degreesToRadians(_ x: CGFloat) -> CGFloat {
    return .pi * x / 180.0
}

func startWiggle(
    duration: Double = 0.25,
    displacement: CGFloat = 1.0,
    degreesRotation: CGFloat = 2.0
    ) {
    let negativeDisplacement = -1.0 * displacement
    let position = CAKeyframeAnimation.init(keyPath: "position")
    position.beginTime = 0.8
    position.duration = duration
    position.values = [
        NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)),
        NSValue(cgPoint: CGPoint(x: 0, y: 0)),
        NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: 0)),
        NSValue(cgPoint: CGPoint(x: 0, y: negativeDisplacement)),
        NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement))
    ]
    position.calculationMode = "linear"
    position.isRemovedOnCompletion = false
    position.repeatCount = Float.greatestFiniteMagnitude
    position.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100))
    position.isAdditive = true

    let transform = CAKeyframeAnimation.init(keyPath: "transform")
    transform.beginTime = 2.6
    transform.duration = duration
    transform.valueFunction = CAValueFunction(name: kCAValueFunctionRotateZ)
    transform.values = [
        degreesToRadians(-1.0 * degreesRotation),
        degreesToRadians(degreesRotation),
        degreesToRadians(-1.0 * degreesRotation)
    ]
    transform.calculationMode = "linear"
    transform.isRemovedOnCompletion = false
    transform.repeatCount = Float.greatestFiniteMagnitude
    transform.isAdditive = true
    transform.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100))

    self.layer.add(position, forKey: nil)
    self.layer.add(transform, forKey: nil)
}
Paul Popiel
  • 930
  • 11
  • 21
7

Paul Popiel gave an excellent answer to this above and I am forever indebted to him for it. There is one small problem I found with his code and that's that it doesn't work well if that routine is called multiple times - the layer animations appear to sometimes get lost or deactivated.

Why call it more than once? I'm implementing it via a UICollectionView, and as the cells are dequeued or moved, I need to reestablish the wiggle. With Paul's original code, my cells would often stop wiggling if they scrolled off screen despite my trying to reestablish the wiggle within the dequeue and the willDisplay callback. However, by giving the two animations named keys, it always works reliably even if called twice on a cell.

This is almost all Paul's code with the above small fix, plus I've created it as an extension to UIView and added a Swift 4 compatible stopWiggle.

private func degreesToRadians(_ x: CGFloat) -> CGFloat {
    return .pi * x / 180.0
}

extension UIView {
    func startWiggle() {
        let duration: Double = 0.25
        let displacement: CGFloat = 1.0
        let degreesRotation: CGFloat = 2.0
        let negativeDisplacement = -1.0 * displacement
        let position = CAKeyframeAnimation.init(keyPath: "position")
        position.beginTime = 0.8
        position.duration = duration
        position.values = [
            NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)),
            NSValue(cgPoint: CGPoint(x: 0, y: 0)),
            NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: 0)),
            NSValue(cgPoint: CGPoint(x: 0, y: negativeDisplacement)),
            NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement))
        ]
        position.calculationMode = "linear"
        position.isRemovedOnCompletion = false
        position.repeatCount = Float.greatestFiniteMagnitude
        position.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100))
        position.isAdditive = true

        let transform = CAKeyframeAnimation.init(keyPath: "transform")
        transform.beginTime = 2.6
        transform.duration = duration
        transform.valueFunction = CAValueFunction(name: kCAValueFunctionRotateZ)
        transform.values = [
            degreesToRadians(-1.0 * degreesRotation),
            degreesToRadians(degreesRotation),
            degreesToRadians(-1.0 * degreesRotation)
        ]
        transform.calculationMode = "linear"
        transform.isRemovedOnCompletion = false
        transform.repeatCount = Float.greatestFiniteMagnitude
        transform.isAdditive = true
        transform.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100))

        self.layer.add(position, forKey: "bounce")
        self.layer.add(transform, forKey: "wiggle")
    }

    func stopWiggle() {
        self.layer.removeAllAnimations()
        self.transform = .identity
    }
}

In case it saves anyone else time implementing this in a UICollectionView, you'll need a few other places to make sure the wiggle stays during moves and scrolls. First, a routine that begins wiggling all the cells that's called at the outset:

func wiggleAllVisibleCells() {
    if let visible = collectionView?.indexPathsForVisibleItems {
        for ip in visible {
            if let cell = collectionView!.cellForItem(at: ip) {
                cell.startWiggle()
            }
        }
    }
}

And as new cells are displayed (from a move or scroll), I reestablish the wiggle:

override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    // Make sure cells are all still wiggling
    if isReordering {
        cell.startWiggle()
    }
}
ajgryc
  • 445
  • 3
  • 6
5

For completeness, here is how I animated my CALayer subclass — inspired by the other answers — using an explicit animation.

-(void)stopJiggle 
{
    [self removeAnimationForKey:@"jiggle"];
}

-(void)startJiggle 
{
    const float amplitude = 1.0f; // degrees
    float r = ( rand() / (float)RAND_MAX ) - 0.5f;
    float angleInDegrees = amplitude * (1.0f + r * 0.1f);
    float animationRotate = angleInDegrees / 180. * M_PI; // Convert to radians

    NSTimeInterval duration = 0.1;
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
    animation.duration = duration;
    animation.additive = YES;
    animation.autoreverses = YES;
    animation.repeatCount = FLT_MAX;
    animation.fromValue = @(-animationRotate);
    animation.toValue = @(animationRotate);
    animation.timeOffset = ( rand() / (float)RAND_MAX ) * duration;
    [self addAnimation:animation forKey:@"jiggle"];
}
Drew McCormack
  • 3,490
  • 1
  • 19
  • 23
4

I reverse engineered Apple Stringboard, and modified little bit their animation, and code below is really good stuff.

+ (CAAnimationGroup *)jiggleAnimation {
CAKeyframeAnimation *position = [CAKeyframeAnimation animation];
position.keyPath = @"position";
position.values = @[
                    [NSValue valueWithCGPoint:CGPointZero],
                    [NSValue valueWithCGPoint:CGPointMake(-1, 0)],
                    [NSValue valueWithCGPoint:CGPointMake(1, 0)],
                    [NSValue valueWithCGPoint:CGPointMake(-1, 1)],
                    [NSValue valueWithCGPoint:CGPointMake(1, -1)],
                    [NSValue valueWithCGPoint:CGPointZero]
                    ];
position.timingFunctions = @[
                             [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
                             [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]
                             ];
position.additive = YES;

CAKeyframeAnimation *rotation = [CAKeyframeAnimation animation];
rotation.keyPath = @"transform.rotation";
rotation.values = @[
                    @0,
                    @0.03,
                    @0,
                    [NSNumber numberWithFloat:-0.02]
                    ];
rotation.timingFunctions = @[
                             [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
                             [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]
                             ];

CAAnimationGroup *group = [[CAAnimationGroup alloc] init];
group.animations = @[ position, rotation ];
group.duration = 0.3;
group.repeatCount = HUGE_VALF;
group.beginTime = arc4random() % 30 / 100.f;
return group;
}

Original Apple jiggle animation:

CAKeyframeAnimation *position = [CAKeyframeAnimation animation];
position.beginTime = 0.8;
position.duration = 0.25;
position.values = @[[NSValue valueWithCGPoint:CGPointMake(-1, -1)],
                    [NSValue valueWithCGPoint:CGPointMake(0, 0)],
                    [NSValue valueWithCGPoint:CGPointMake(-1, 0)],
                    [NSValue valueWithCGPoint:CGPointMake(0, -1)],
                    [NSValue valueWithCGPoint:CGPointMake(-1, -1)]];
position.calculationMode = @"linear";
position.removedOnCompletion = NO;
position.repeatCount = CGFLOAT_MAX;
position.beginTime = arc4random() % 25 / 100.f;
position.additive = YES;
position.keyPath = @"position";

CAKeyframeAnimation *transform = [CAKeyframeAnimation animation];
transform.beginTime = 2.6;
transform.duration = 0.25;
transform.valueFunction = [CAValueFunction functionWithName:kCAValueFunctionRotateZ];
transform.values = @[@(-0.03525565),@(0.03525565),@(-0.03525565)];
transform.calculationMode = @"linear";
transform.removedOnCompletion = NO;
transform.repeatCount = CGFLOAT_MAX;
transform.additive = YES;
transform.beginTime = arc4random() % 25 / 100.f;
transform.keyPath = @"transform";

[self.dupa.layer addAnimation:position forKey:nil];
[self.dupa.layer addAnimation:transform forKey:nil];
Michal Zaborowski
  • 5,039
  • 36
  • 35
3

So I'm sure I'll get yelled at for writing messy code (there are probably simpler ways to do this that I am not aware of because I'm a semi-beginner), but this is a more random version of Vic320's algorithm that varies the amount of rotation and translation. It also decides randomly which direction it will wobble first, which gives a much more random look if you have multiple things wobbling simultaneously. If efficiency is a big problem for you, do not use. This is just what I came up with with the way that I know how to do it.

For anyone wondering you need to #import <QuartzCore/QuartzCore.h> and add it to your linked libraries.

#define degreesToRadians(x) (M_PI * (x) / 180.0)

- (void)startJiggling:(NSInteger)count {
    double kAnimationRotateDeg = (double)(arc4random()%5 + 5) / 10;
    double kAnimationTranslateX = (arc4random()%4);
    double kAnimationTranslateY = (arc4random()%4);

    CGAffineTransform leftWobble = CGAffineTransformMakeRotation(degreesToRadians( kAnimationRotateDeg * (count%2 ? +1 : -1 ) ));
    CGAffineTransform rightWobble = CGAffineTransformMakeRotation(degreesToRadians( kAnimationRotateDeg * (count%2 ? -1 : +1 ) ));
    int leftOrRight = (arc4random()%2);
    if (leftOrRight == 0){
        CGAffineTransform moveTransform = CGAffineTransformTranslate(rightWobble, -kAnimationTranslateX, -kAnimationTranslateY);
        CGAffineTransform conCatTransform = CGAffineTransformConcat(rightWobble, moveTransform);
        self.transform = leftWobble;  // starting point

        [UIView animateWithDuration:0.1
                              delay:(count * 0.08)
                            options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionRepeat | UIViewAnimationOptionAutoreverse
                         animations:^{ self.transform = conCatTransform; }
                         completion:nil];
    } else if (leftOrRight == 1) {
        CGAffineTransform moveTransform = CGAffineTransformTranslate(leftWobble, -kAnimationTranslateX, -kAnimationTranslateY);
        CGAffineTransform conCatTransform = CGAffineTransformConcat(leftWobble, moveTransform);
        self.transform = rightWobble;  // starting point

        [UIView animateWithDuration:0.1
                              delay:(count * 0.08)
                            options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionRepeat | UIViewAnimationOptionAutoreverse
                         animations:^{ self.transform = conCatTransform; }
                         completion:nil];
    }
}

- (void)stopJiggling {
    [self.layer removeAllAnimations];
    self.transform = CGAffineTransformIdentity;  // Set it straight 
}
Josh Sherick
  • 2,161
  • 3
  • 20
  • 37
  • I like your choice to randomize which direction to begin jiggling. I'm going to go update my code similarly!! – tobinjim Jun 28 '12 at 20:04
2

For the benefit of others who come along in the future, I felt the jiggle offered by @Vic320 was a little too robotic and comparing it to Keynote it was a little too strong and not organic (random?) enough. So in the spirit of sharing, here is the code I built into my subclass of UIView... my view controller keeps an array of these objects and when the user taps the Edit button, the view controller sends the startJiggling message to each, followed by a stopJiggling message when the user presses the Done button.

- (void)startJiggling
{
    // jiggling code based off the folks on stackoverflow.com:
    // http://stackoverflow.com/questions/6604356/ios-icon-jiggle-algorithm

#define degreesToRadians(x) (M_PI * (x) / 180.0)
#define kAnimationRotateDeg 0.1
    jiggling = YES;
    [self wobbleLeft];
}

- (void)wobbleLeft
{
    if (jiggling) {
        NSInteger randomInt = arc4random()%500;
        float r = (randomInt/500.0)+0.5;

        CGAffineTransform leftWobble = CGAffineTransformMakeRotation(degreesToRadians( (kAnimationRotateDeg * -1.0) - r ));
        CGAffineTransform rightWobble = CGAffineTransformMakeRotation(degreesToRadians( kAnimationRotateDeg + r ));

        self.transform = leftWobble;  // starting point

        [UIView animateWithDuration:0.1
                      delay:0
                    options:UIViewAnimationOptionAllowUserInteraction
                 animations:^{ self.transform = rightWobble; }
                 completion:^(BOOL finished) { [self wobbleRight]; }
          ];
    }
}

- (void)wobbleRight
{
    if (jiggling) {

        NSInteger randomInt = arc4random()%500;
        float r = (randomInt/500.0)+0.5;

        CGAffineTransform leftWobble = CGAffineTransformMakeRotation(degreesToRadians( (kAnimationRotateDeg * -1.0) - r ));
        CGAffineTransform rightWobble = CGAffineTransformMakeRotation(degreesToRadians( kAnimationRotateDeg + r ));

        self.transform = rightWobble;  // starting point

        [UIView animateWithDuration:0.1
                      delay:0
                    options:UIViewAnimationOptionAllowUserInteraction
                 animations:^{ self.transform = leftWobble; }
                 completion:^(BOOL finished) { [self wobbleLeft]; }
         ];
    }

}
- (void)stopJiggling
{
    jiggling = NO;
    [self.layer removeAllAnimations];
    [self setTransform:CGAffineTransformIdentity];
    [self.layer setAnchorPoint:CGPointMake(0.5, 0.5)];
}
tobinjim
  • 1,862
  • 2
  • 19
  • 31
2

Check out the openspringboard project.

In particular, setIconAnimation:(BOOL)isAnimating in OpenSpringBoard.m. That should give you some ideas on how to do this.

Community
  • 1
  • 1
sosborn
  • 14,676
  • 2
  • 42
  • 46
1

In case anyone needs the same code in Swift

class Animation {

    static func wiggle(_ btn: UIButton) {
        btn.startWiggling()
    }
} 

extension UIView {

func startWiggling() {

    let count = 5
    let kAnimationRotateDeg = 1.0

    let leftDegrees = (kAnimationRotateDeg * ((count%2 > 0) ? +5 : -5)).convertToDegrees()
    let leftWobble = CGAffineTransform(rotationAngle: leftDegrees)

    let rightDegrees = (kAnimationRotateDeg * ((count%2 > 0) ? -10 : +10)).convertToDegrees()
    let rightWobble = CGAffineTransform(rotationAngle: rightDegrees)

    let moveTransform = rightWobble.translatedBy(x: -2.0, y: 2.0)
    let concatTransform = rightWobble.concatenating(moveTransform)

    self.transform = leftWobble

    UIView.animate(withDuration: 0.1,
                   delay: 0.1,
                   options: [.allowUserInteraction, .repeat, .autoreverse],
                   animations: {
                    UIView.setAnimationRepeatCount(3)
                    self.transform = concatTransform
    }, completion: { success in
        self.layer.removeAllAnimations()
        self.transform = .identity
    })
}
}

Just Call

    Animation.wiggle(viewToBeAnimated)

It is always best to write a wrapper over the functions you are calling so that even if you have to change the function arguments or may be the name of the function, it does not take you to rewrite it everywhere in the code.

Swati
  • 1,417
  • 1
  • 15
  • 35
0

Here is the Swift 4.2 version of @mientus' code (which is itself an update of Paul Popiel's version), as an extension of CALayer:

extension CALayer {

    private enum WigglingAnimationKey: String {
        case position = "wiggling_position_animation"
        case transform = "wiggling_transform_animation"
    }

    func startWiggling() {
        let duration = 0.25
        let displacement = 1.0
        let negativeDisplacement = displacement * -1
        let rotationAngle = Measurement(value: 2, unit: UnitAngle.degrees)

        // Position animation
        let positionAnimation = CAKeyframeAnimation(keyPath: #keyPath(CALayer.position))
        positionAnimation.beginTime = 0.8
        positionAnimation.duration = duration
        positionAnimation.values = [
            NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)),
            NSValue(cgPoint: CGPoint.zero),
            NSValue(cgPoint: CGPoint(x: 0, y: negativeDisplacement)),
            NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement))
        ]
        positionAnimation.calculationMode = .linear
        positionAnimation.isRemovedOnCompletion = false
        positionAnimation.repeatCount = .greatestFiniteMagnitude
        positionAnimation.beginTime = CFTimeInterval(Float(Int.random(in: 0...25)) / 100)
        positionAnimation.isAdditive = true

        // Rotation animation
        let transformAnimation = CAKeyframeAnimation(keyPath: #keyPath(CALayer.transform))
        transformAnimation.beginTime = 2.6
        transformAnimation.duration = duration
        transformAnimation.valueFunction = CAValueFunction(name: .rotateZ)
        transformAnimation.values = [
            CGFloat(rotationAngle.converted(to: .radians).value * -1),
            CGFloat(rotationAngle.converted(to: .radians).value),
            CGFloat(rotationAngle.converted(to: .radians).value * -1)
        ]
        transformAnimation.calculationMode = .linear
        transformAnimation.isRemovedOnCompletion = false
        transformAnimation.repeatCount = .greatestFiniteMagnitude
        transformAnimation.isAdditive = true
        transformAnimation.beginTime = CFTimeInterval(Float(Int.random(in: 0...25)) / 100)

        self.add(positionAnimation, forKey: WigglingAnimationKey.position.rawValue)
        self.add(transformAnimation, forKey: WigglingAnimationKey.transform.rawValue)
    }

    func stopWiggling() {
        self.removeAnimation(forKey: WigglingAnimationKey.position.rawValue)
        self.removeAnimation(forKey: WigglingAnimationKey.transform.rawValue)
    }
}

Usage (where anyLayer is a CALayer):

// Start animating.
anyLayer.startWiggling()
// Stop animating.
anyLayer.stopWiggling()
Vin Gazoil
  • 1,942
  • 2
  • 20
  • 24