CAAnimation stops when scrolled out of bounds - iphone

I have a custom activity indicator in the form of an infinitely rotating UIImageView.
And this this amazing activity indicator is placed inside a table cell.
The animation is achieved this way:
CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
animation.fromValue = [NSNumber numberWithFloat:0.0f];
animation.toValue = [NSNumber numberWithFloat: M_PI * 2.0];
animation.duration = 1.0f;
animation.repeatCount = INFINITY;
[self.layer addAnimation:animation forKey:#"SpinAnimation"];
At first, it work beautifully... But when the cell gets scrolled off the bounds of the table, the animation stops, and I have no idea how to reset it... /:
Please help...
Thanks so much in advance... :)

The reason is animation for rotation.z key became nil when prepareForReuse made its work or your application came into background mode.
Consider to draw activity indicator by hands or try something like https://github.com/airbnb/lottie-ios
The approach below activity indicator works and rolling fine inside scroll / cells / background mode
I tried rotation.z and something workarounds based on CAReplicationLayer - both failed in scroll / cells / background mode
private func drawLoaderCircle() {
let path = UIBezierPath(arcCenter: loaderViewCenter, radius: loaderCircleRadius, startAngle: 0, endAngle: 2 * .pi, clockwise: true).cgPath
setupGrapes(endAngle: 310, path: path)
}
private func setupGrapes(endAngle: CGFloat, path: CGPath) {
let step: CGFloat = 2
var grapeAnimationDelay: CFTimeInterval = 0
for i in stride(from: 0, to: endAngle, by: step) {
let radians = i * .pi / 180
let x = loaderViewCenter.x + loaderCircleRadius * cos(radians)
let y = loaderViewCenter.y + loaderCircleRadius * sin(radians)
let grapeLayer = CAShapeLayer()
grapeLayer.frame = CGRect(x: 0, y: 0, width: circleWidth + 0.2, height: circleWidth + 0.2)
grapeLayer.position = CGPoint(x: x, y: y)
grapeLayer.cornerRadius = grapeLayer.frame.width / 2
grapeLayer.backgroundColor = loaderColor.cgColor
grapeAnimationDelay += Double(rotateDuration * Double(step) / 360)
let rotateGrapeAnimation = CAKeyframeAnimation(keyPath: "position")
rotateGrapeAnimation.path = path
rotateGrapeAnimation.beginTime = grapeAnimationDelay
rotateGrapeAnimation.calculationMode = .linear
rotateGrapeAnimation.repeatCount = .infinity
rotateGrapeAnimation.isRemovedOnCompletion = false
rotateGrapeAnimation.duration = rotateDuration
grapeLayer.add(rotateGrapeAnimation, forKey: nil)
progressLayer.addSublayer(grapeLayer)
}
}

It seems that this animation is removed when corresponding cell is reused. At least, I've got animationDidStop delegate's callback when I returned dequeued cell from cellForItemAtIndexPath.
The simplest workaround I can come up with:
Your table view data source:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
if(cell && [cell conformsToProtocol:#protocol(CustomCellProtocol)])
{
[(id<CustomCellProtocol>)cell beforeDisplayed];
}
return cell;
}
Your cell:
static NSString* const kSpinAnimationKey = #"SpinAnimation";
#interface CustomCell () <CustomCellProtocol>
#end
#implementation CustomCell
-(void)startSpinAnimation
{
CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
animation.fromValue = [NSNumber numberWithFloat:0.0f];
animation.toValue = [NSNumber numberWithFloat: M_PI * 2.0];
animation.duration = 1.0f;
animation.repeatCount = INFINITY;
[self.layer addAnimation:animation forKey:kSpinAnimationKey];
}
-(void)beforeDisplayed
{
UIView* animatedView = self.contentView;
if(![animatedView.layer animationForKey:kSpinAnimationKey])
{
[self startSpinAnimation];
}
}

Related

How to create a top fade effect using UIScrollView?

I've got a UIScrollView in my app and I have seen in some other apps that when the user scrolls, the top section fades out on scroll rather than just dissapearing out.
I really love this effect and want to achieve it. Any ideas how it can be done?
Simple 2020 solution:
import UIKit
class FadeTail: UIIView {
private lazy var gradientLayer: CAGradientLayer = {
let l = CAGradientLayer()
l.startPoint = CGPoint(x: 0.5, y: 0)
l.endPoint = CGPoint(x: 0.5, y: 1)
let baseColor = UIColor.white // for example
l.colors = [
baseColor.withAlphaComponent(0),
baseColor.withAlphaComponent(1),
].map{$0.cgColor}
layer.addSublayer(l)
return l
}()
override func layoutSubviews() {
super.layoutSubviews()
gradientLayer.frame = bounds
}
}
Simply
autolayout that view in storyboard,
any shape or size you wish
on top of the view (text, image, webview, anything) you wish to be faded.
Easy.
Tip - gradients, circles, etc
If you need crazy circular/banded/etc fades, use the techniques here: https://stackoverflow.com/a/61174086/294884
EDIT: I've put this code up on github, see here.
See my answer to a similar question.
My solution is to subclass UIScrollView, and create a mask layer in the layoutSubviews method.
#import "FadingScrollView.h"
#import <QuartzCore/QuartzCore.h>
static float const fadePercentage = 0.2;
#implementation FadingScrollView
// ...
- (void)layoutSubviews
{
[super layoutSubviews];
NSObject * transparent = (NSObject *) [[UIColor colorWithWhite:0 alpha:0] CGColor];
NSObject * opaque = (NSObject *) [[UIColor colorWithWhite:0 alpha:1] CGColor];
CALayer * maskLayer = [CALayer layer];
maskLayer.frame = self.bounds;
CAGradientLayer * gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = CGRectMake(self.bounds.origin.x, 0,
self.bounds.size.width, self.bounds.size.height);
gradientLayer.colors = [NSArray arrayWithObjects: transparent, opaque,
opaque, transparent, nil];
// Set percentage of scrollview that fades at top & bottom
gradientLayer.locations = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0],
[NSNumber numberWithFloat:fadePercentage],
[NSNumber numberWithFloat:1.0 - fadePercentage],
[NSNumber numberWithFloat:1], nil];
[maskLayer addSublayer:gradientLayer];
self.layer.mask = maskLayer;
}
#end
The code above fades the top and bottom of the UIScrollView from the background colour to transparent, but this can be easily changed to fade the top only (or fade to any colour you want).
Change this line to fade the top only:
// Fade top of scrollview only
gradientLayer.locations = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0],
[NSNumber numberWithFloat:fadePercentage],
[NSNumber numberWithFloat:1],
[NSNumber numberWithFloat:1], nil];
EDIT 2:
Or fade the top only by changing these two lines:
// Fade top of scrollview only
gradientLayer.colors = [NSArray arrayWithObjects: transparent, opaque, nil];
gradientLayer.locations = [NSArray arrayWithObjects: [NSNumber numberWithFloat:0],
[NSNumber numberWithFloat:fadePercentage], nil];
Or, fade the bottom only:
// Fade bottom of scrollview only
gradientLayer.colors = [NSArray arrayWithObjects: opaque, transparent, nil];
gradientLayer.locations = [NSArray arrayWithObjects: [NSNumber numberWithFloat:1.0 - fadePercentage],
[NSNumber numberWithFloat:1], nil];
You can use a CAGradientLayer by
Adding the QuartzCore.framework to your project (see Linking to Library or Framework).
Add #import of the QuartzCore headers:
#import <QuartzCore/QuartzCore.h>
And then use CAGradientLayer:
- (void)addGradientMaskToView:(UIView *)view
{
CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.frame = view.bounds;
gradient.colors = #[(id)[[UIColor clearColor] CGColor], (id)[[UIColor whiteColor] CGColor]];
gradient.startPoint = CGPointMake(0.5, 0.0); // this is the default value, so this line is not needed
gradient.endPoint = CGPointMake(0.5, 0.20);
[view.layer setMask:gradient];
}
Note, this CAGradientLayer is a gradient from a color with alpha of 0.0 (e.g. clearColor) to a color to a color with alpha of 1.0 (e.g. whiteColor), not just from black to white. You can adjust the startPoint (the default value is probably fine) and the endPoint to adjust where you want the gradient to be applied.
And generally, when doing this with a UIScrollView, unless you want the gradient to scroll with you, you make the UIScrollView a subview of some other UIView and apply this gradient to that container view, not the scroll view itself.
Thanks to Fattie's answer I created the following UIView extension, in Swift, that takes care of the gradient fading and provides more styles (bottom, top, left right, vertical and horizontal) as well as fade percentage.
For any comments/recommendations, please let me know at the gist I've created, I try to keep both that and this answer up to date with any changes I add.
The Extension:
extension UIView {
enum UIViewFadeStyle {
case bottom
case top
case left
case right
case vertical
case horizontal
}
func fadeView(style: UIViewFadeStyle = .bottom, percentage: Double = 0.07) {
let gradient = CAGradientLayer()
gradient.frame = bounds
gradient.colors = [UIColor.white.cgColor, UIColor.clear.cgColor]
let startLocation = percentage
let endLocation = 1 - percentage
switch style {
case .bottom:
gradient.startPoint = CGPoint(x: 0.5, y: endLocation)
gradient.endPoint = CGPoint(x: 0.5, y: 1)
case .top:
gradient.startPoint = CGPoint(x: 0.5, y: startLocation)
gradient.endPoint = CGPoint(x: 0.5, y: 0.0)
case .vertical:
gradient.startPoint = CGPoint(x: 0.5, y: 0.0)
gradient.endPoint = CGPoint(x: 0.5, y: 1.0)
gradient.colors = [UIColor.clear.cgColor, UIColor.white.cgColor, UIColor.white.cgColor, UIColor.clear.cgColor]
gradient.locations = [0.0, startLocation, endLocation, 1.0] as [NSNumber]
case .left:
gradient.startPoint = CGPoint(x: startLocation, y: 0.5)
gradient.endPoint = CGPoint(x: 0.0, y: 0.5)
case .right:
gradient.startPoint = CGPoint(x: endLocation, y: 0.5)
gradient.endPoint = CGPoint(x: 1, y: 0.5)
case .horizontal:
gradient.startPoint = CGPoint(x: 0.0, y: 0.5)
gradient.endPoint = CGPoint(x: 1.0, y: 0.5)
gradient.colors = [UIColor.clear.cgColor, UIColor.white.cgColor, UIColor.white.cgColor, UIColor.clear.cgColor]
gradient.locations = [0.0, startLocation, endLocation, 1.0] as [NSNumber]
}
layer.mask = gradient
}
}
You add an alpha mask layer to a view containing your scroll view like this:
CALayer *mask = [CALayer layer];
CGImageRef maskRef = [UIImage imageNamed:#"scrollMask"].CGImage;
CGImageRef maskImage = CGImageMaskCreate(CGImageGetWidth(maskRef),
CGImageGetHeight(maskRef),
CGImageGetBitsPerComponent(maskRef),
CGImageGetBitsPerPixel(maskRef),
CGImageGetBytesPerRow(maskRef),
CGImageGetDataProvider(maskRef), NULL, false);
mask.contents = (__bridge id)maskImage;
mask.frame = CGRectMake(0, 0, view.bounds.size.width, view.bounds.size.height);
view.layer.mask = mask;
view.layer.masksToBounds = YES;
CGImageRelease(maskImage);
where "scrollMask" is a grayscale image defining mask region: white == fully masked, black == not masked at all and gray == partially masked.
To create the effect you're looking for, the mask image would be black with a white gradient at the top like this:
For more details, take a look at the documentation for CGImageMaskCreate.
We built onto Steph Sharp's code to only fade when necessary. Our code is setup as a static utility method instead of a subclass so that we could reuse the method in our subclasses of UIScrollView and UITableView. Here is our static utility method:
#define kScrollViewFadeColorLight [UIColor colorWithRed:0.56 green:0.56 blue:0.56 alpha:0.0]
#define kScrollViewFadeColorDark [UIColor colorWithRed:0.56 green:0.56 blue:0.56 alpha:1.0]
#define kScrollViewScrollBarWidth 7.0
#define kScrollViewFadingEdgeLength 40.0
+ (void) applyFadeToScrollView:(UIScrollView*) scrollView {
CGFloat topOffset = -scrollView.contentInset.top;
CGFloat bottomOffset = scrollView.contentSize.height + scrollView.contentInset.bottom - scrollView.bounds.size.height;
CGFloat distanceFromTop = scrollView.contentOffset.y - topOffset;
CGFloat distanceFromBottom = bottomOffset - scrollView.contentOffset.y;
BOOL isAtTop = distanceFromTop < 1.0;
BOOL isAtBottom = distanceFromBottom < 1.0;
if (isAtTop && isAtBottom) {
// There is no scrolling to be done here, so don't fade anything!
scrollView.layer.mask = nil;
return;
}
NSObject* transparent = (NSObject*)[kScrollViewFadeColorLight CGColor];
NSObject* opaque = (NSObject*)[kScrollViewFadeColorDark CGColor];
CALayer* maskLayer = [CALayer layer];
maskLayer.frame = scrollView.bounds;
CALayer* scrollGutterLayer = [CALayer layer];
scrollGutterLayer.frame = CGRectMake(scrollView.bounds.size.width - kScrollViewScrollBarWidth, 0.0,
kScrollViewScrollBarWidth, scrollView.bounds.size.height);
scrollGutterLayer.backgroundColor = (__bridge CGColorRef)(opaque);
[maskLayer addSublayer:scrollGutterLayer];
CAGradientLayer* gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = CGRectMake(0.0, 0.0, scrollView.bounds.size.width, scrollView.bounds.size.height);
CGFloat fadePercentage = kScrollViewFadingEdgeLength / scrollView.bounds.size.height;
NSMutableArray* colors = [[NSMutableArray alloc] init];
NSMutableArray* locations = [[NSMutableArray alloc] init];
if (!isAtTop) {
[colors addObjectsFromArray:#[transparent, opaque]];
[locations addObjectsFromArray:#[#0.0, [NSNumber numberWithFloat:fadePercentage]]];
}
if (!isAtBottom) {
[colors addObjectsFromArray:#[opaque, transparent]];
[locations addObjectsFromArray:#[[NSNumber numberWithFloat:1.0 - fadePercentage], #1.0]];
}
gradientLayer.colors = colors;
gradientLayer.locations = locations;
[maskLayer addSublayer:gradientLayer];
scrollView.layer.mask = maskLayer;
}
Here is our usage of that method.
FadingScrollView.h
#interface FadingScrollView : UIScrollView
#end
FadingScrollView.m
#implementation FadingScrollView
- (void) layoutSubviews {
[super layoutSubviews];
[Utils applyFadeToScrollView:self];
}
#end
FadingTableView.h
#interface FadingTableView : UITableView
#end
FadingTableView.m
#implementation FadingTableView
- (void) layoutSubviews {
[super layoutSubviews];
[Utils applyFadeToScrollView:self];
}
#end

having trouble creating ken burns effect with CALayer for iphone

I have been trying for the last few days to create the ken burns effect using a CALayer with animations and then save it to a video file.
I have my image layer which is inside another layer that is 1024x576. All of the animations are applied to the image layer.
Here is the code so far:
- (CALayer*)buildKenBurnsLayerWithImage:(UIImage *)image startPoint:(CGPoint)startPoint endPoint:(CGPoint)endPoint fromScale:(float)fromScale toScale:(float)toScale
{
float calFromScale = fromScale + 1;
float calToScale = toScale + 1;
float fromX = startPoint.x * calFromScale;
float fromY = (image.size.height * calFromScale) - (videoSize.height + (startPoint.y * calFromScale));
float toX = endPoint.x * calToScale;
float toY = (image.size.height * calToScale) - (videoSize.height + (endPoint.y * calToScale));
CGPoint anchor = CGPointMake(0.0, 0.0);
CALayer* imageLayer = [CALayer layer];
imageLayer.contents = (id)image.CGImage;
imageLayer.anchorPoint = anchor;
imageLayer.bounds = CGRectMake(0.0, 0.0, image.size.width, image.size.height);
imageLayer.position = CGPointMake(image.size.width * anchor.x, image.size.height * anchor.y);
imageLayer.contentsGravity = kCAGravityResizeAspect;
// create the panning animation.
CABasicAnimation* panningAnimation = [CABasicAnimation animationWithKeyPath:#"position"];
panningAnimation.fromValue = [NSValue valueWithCGPoint:CGPointMake(-fromX, -fromY)];
panningAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(-toX, -toY)];
panningAnimation.additive = YES;
panningAnimation.removedOnCompletion = NO;
// create the scale animation.
CABasicAnimation* scaleAnimation = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
scaleAnimation.fromValue = [NSNumber numberWithFloat:fromScale];
scaleAnimation.toValue = [NSNumber numberWithFloat:toScale];
scaleAnimation.additive = YES;
scaleAnimation.removedOnCompletion = NO;
CAAnimationGroup* animationGroup = [CAAnimationGroup animation];
animationGroup.animations = [[NSMutableArray alloc] initWithObjects:panningAnimation,scaleAnimation, nil];
animationGroup.beginTime = 1e-100;
animationGroup.duration = 5.0;
animationGroup.removedOnCompletion = NO;
animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[imageLayer addAnimation:animationGroup forKey:nil];
return imageLayer;
}
Here is how i'm calling the method:
CALayer* animatedLayer = [self buildKenBurnsLayerWithImage:image startPoint:CGPointMake(100, 100) endPoint:CGPointMake(500, 500) fromScale:5.0 toScale:2.0];
The problem I am having is that the end result with panning and scaling is off by a few pixels on the screen.
If someone knows how to fix this i would great appreciate it.
All transformations are applied with respect to the anchor point. Try using the anchor point
CGPoint anchor = CGPointMake(0.5f, 0.5f);
Your "viewport" should no longer scale to the bottom right (if that is responsible for the animation being off by a few pixels), but equally to all directions.

iPhone SDK : Create Wiggle Effect [duplicate]

We are currently developing an application that contains a series of icons. We want the icons to wiggle like the app deletion animations when pressed. What would be the best way to code this animation sequence?
The answer by Vinzius is very cool. However the wobble only rotates from 0 Radians to 0.08. Thus the wobble can look a little unbalanced. If you get this same issue then you may want to add both a negative and a positive rotation by using a CAKeyframeAnimation rather than a CABasicRotation:
- (CAAnimation*)getShakeAnimation
{
CAKeyframeAnimation* animation = [CAKeyframeAnimation animationWithKeyPath:#"transform"];
CGFloat wobbleAngle = 0.06f;
NSValue* valLeft = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(wobbleAngle, 0.0f, 0.0f, 1.0f)];
NSValue* valRight = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(-wobbleAngle, 0.0f, 0.0f, 1.0f)];
animation.values = [NSArray arrayWithObjects:valLeft, valRight, nil];
animation.autoreverses = YES;
animation.duration = 0.125;
animation.repeatCount = HUGE_VALF;
return animation;
}
You can use this animation method for your view or button like this.
[self.yourbutton.layer addAnimation:[self getShakeAnimation] forKey:#""];
SWIFT :-
let transformAnim = CAKeyframeAnimation(keyPath:"transform")
transformAnim.values = [NSValue(CATransform3D: CATransform3DMakeRotation(0.04, 0.0, 0.0, 1.0)),NSValue(CATransform3D: CATransform3DMakeRotation(-0.04 , 0, 0, 1))]
transformAnim.autoreverses = true
transformAnim.duration = (Double(indexPath.row)%2) == 0 ? 0.115 : 0.105
transformAnim.repeatCount = Float.infinity
self.layer.addAnimation(transformAnim, forKey: "transform")
Objective C :-
-(CAKeyframeAnimation *)wiggleView
{
CAKeyframeAnimation* animation = [CAKeyframeAnimation animationWithKeyPath:#"transform"];
CGFloat wobbleAngle = 0.04f;
NSValue* valLeft = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(wobbleAngle, 0.0f, 0.0f, 1.0f)];
NSValue* valRight = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(-wobbleAngle, 0.0f, 0.0f, 1.0f)];
animation.values = [NSArray arrayWithObjects:valLeft, valRight, nil];
animation.autoreverses = YES;
animation.duration = 0.125;
animation.repeatCount = HUGE_VALF;
return animation;
}
Looking at the iOS implementation a bit closer, there are two things that make theirs a bit more realistic than the code mentioned here:
The icons appear to have a bounce as well as a rotation
Every icon has its own timing -- they are not all synchronized
I based myself on the answers here (and with some help from this answer) to add the rotation, the bounce and a bit of randomness to the duration of each animation.
#define kWiggleBounceY 4.0f
#define kWiggleBounceDuration 0.12
#define kWiggleBounceDurationVariance 0.025
#define kWiggleRotateAngle 0.06f
#define kWiggleRotateDuration 0.1
#define kWiggleRotateDurationVariance 0.025
-(void)startWiggling {
[UIView animateWithDuration:0
animations:^{
[self.layer addAnimation:[self rotationAnimation] forKey:#"rotation"];
[self.layer addAnimation:[self bounceAnimation] forKey:#"bounce"];
self.transform = CGAffineTransformIdentity;
}];
}
-(CAAnimation*)rotationAnimation {
CAKeyframeAnimation* animation = [CAKeyframeAnimation animationWithKeyPath:#"transform.rotation.z"];
animation.values = #[#(-kWiggleRotateAngle), #(kWiggleRotateAngle)];
animation.autoreverses = YES;
animation.duration = [self randomizeInterval:kWiggleRotateDuration
withVariance:kWiggleRotateDurationVariance];
animation.repeatCount = HUGE_VALF;
return animation;
}
-(CAAnimation*)bounceAnimation {
CAKeyframeAnimation* animation = [CAKeyframeAnimation animationWithKeyPath:#"transform.translation.y"];
animation.values = #[#(kWiggleBounceY), #(0.0)];
animation.autoreverses = YES;
animation.duration = [self randomizeInterval:kWiggleBounceDuration
withVariance:kWiggleBounceDurationVariance];
animation.repeatCount = HUGE_VALF;
return animation;
}
-(NSTimeInterval)randomizeInterval:(NSTimeInterval)interval withVariance:(double)variance {
double random = (arc4random_uniform(1000) - 500.0) / 500.0;
return interval + variance * random;
}
I implemented this code on a UICollectionView which had 30 items bouncing and the performance was flawless on an iPad 2.
I tried to do something like that for an iPad app.
I tried to do some rotations (with CAAnimation) to the view. Here is a sample code I wrote :
- (CAAnimation*)getShakeAnimation {
CABasicAnimation *animation;
CATransform3D transform;
// Create the rotation matrix
transform = CATransform3DMakeRotation(0.08, 0, 0, 1.0);
// Create a basic animation to animate the layer's transform
animation = [CABasicAnimation animationWithKeyPath:#"transform"];
// Assign the transform as the animation's value
animation.toValue = [NSValue valueWithCATransform3D:transform];
animation.autoreverses = YES;
animation.duration = 0.1;
animation.repeatCount = HUGE_VALF;
return animation;
}
And you should try to apply this one to your layer (with function : addAnimation).
Here, autoreverses property is to alternate left and right orientation.
Try setting others values to the angle and duration.
But in my case I had to add others angles to the CATransform3DMakeRotation method, depending on the initial layer orientation ^^
Good Luck !
Vincent
Rewrote Sebastien's answer in Swift 3.
let wiggleBounceY = 4.0
let wiggleBounceDuration = 0.12
let wiggleBounceDurationVariance = 0.025
let wiggleRotateAngle = 0.06
let wiggleRotateDuration = 0.10
let wiggleRotateDurationVariance = 0.025
func randomize(interval: TimeInterval, withVariance variance: Double) -> Double{
let random = (Double(arc4random_uniform(1000)) - 500.0) / 500.0
return interval + variance * random
}
func startWiggle(for view: UIView){
//Create rotation animation
let rotationAnim = CAKeyframeAnimation(keyPath: "transform.rotation.z")
rotationAnim.values = [-wiggleRotateAngle, wiggleRotateAngle]
rotationAnim.autoreverses = true
rotationAnim.duration = randomize(interval: wiggleRotateDuration, withVariance: wiggleRotateDurationVariance)
rotationAnim.repeatCount = HUGE
//Create bounce animation
let bounceAnimation = CAKeyframeAnimation(keyPath: "transform.translation.y")
bounceAnimation.values = [wiggleBounceY, 0]
bounceAnimation.autoreverses = true
bounceAnimation.duration = randomize(interval: wiggleBounceDuration, withVariance: wiggleBounceDurationVariance)
bounceAnimation.repeatCount = HUGE
//Apply animations to view
UIView.animate(withDuration: 0) {
view.layer.add(rotationAnim, forKey: "rotation")
view.layer.add(bounceAnimation, forKey: "bounce")
view.transform = .identity
}
}
func stopWiggle(for view: UIView){
view.layer.removeAllAnimations()
}
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()
}
func randomInterval(_ interval: TimeInterval, variance: Double) -> TimeInterval {
return interval + variance * Double((Double(arc4random_uniform(1000)) - 500.0) / 500.0)
}
Look at this iOS SpingBoard example
Answered in another thread a Swift 4 version of what is apparently Apple's own algorithm reverse engineered:
https://stackoverflow.com/a/47730519/5018607
Edited paiego's code to fit my needs: visual error animation feedback upon user's action (tap). It happens once - it's not a constant wiggling like SpringBoard app edit wiggle animation.
- (CAAnimation *)shakeAnimation {
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"transform"];
CGFloat wobbleAngle = 0.06f;
NSValue *valLeft;
NSValue *valRight;
NSMutableArray *values = [NSMutableArray new];
for (int i = 0; i < 5; i++) {
valLeft = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(wobbleAngle, 0.0f, 0.0f, 1.0f)];
valRight = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(-wobbleAngle, 0.0f, 0.0f, 1.0f)];
[values addObjectsFromArray:#[valLeft, valRight]];
wobbleAngle*=0.66;
}
animation.values = [values copy];
animation.duration = 0.7;
return animation;
}
Usage:
[your_view.layer addAnimation:[self shakeAnimation] forKey:#""]; //do the shake animation
your_view.transform = CGAffineTransformIdentity; //return the view back to original
Hope this helps someone else.

How to animate the frame of an layer with CABasicAnimation?

I guess I have to convert the CGRect into an object to pass it to fromValue?
This is how I try it, but it doesn't work:
CABasicAnimation *frameAnimation = [CABasicAnimation animationWithKeyPath:#"frame"];
frameAnimation.duration = 2.5;
frameAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
frameAnimation.fromValue = [NSValue valueWithCGRect:myLayer.frame];
frameAnimation.toValue = [NSValue valueWithCGRect:theNewFrameRect];
[myLayer addAnimation:frameAnimation forKey:#"MLC"];
The frame property of a CALayer is a derived property, dependent on the position, anchorPoint, bounds and transform of the layer. Instead of animating the frame, you should instead animate the position or bounds, depending on what effect you are trying to accomplish.
To move a layer, you can animate the position:
-(void)moveLayer:(CALayer*)layer to:(CGPoint)point
{
// Prepare the animation from the current position to the new position
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position"];
animation.fromValue = [layer valueForKey:#"position"];
// NSValue/+valueWithPoint:(NSPoint)point is available on Mac OS X
// NSValue/+valueWithCGPoint:(CGPoint)point is available on iOS
// comment/uncomment the corresponding lines depending on which platform you're targeting
// Mac OS X
animation.toValue = [NSValue valueWithPoint:NSPointFromCGPoint(point)];
// iOS
//animation.toValue = [NSValue valueWithCGPoint:point];
// Update the layer's position so that the layer doesn't snap back when the animation completes.
layer.position = point;
// Add the animation, overriding the implicit animation.
[layer addAnimation:animation forKey:#"position"];
}
To resize a layer, you would animate the bounds parameter:
-(void)resizeLayer:(CALayer*)layer to:(CGSize)size
{
// Prepare the animation from the old size to the new size
CGRect oldBounds = layer.bounds;
CGRect newBounds = oldBounds;
newBounds.size = size;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"bounds"];
// NSValue/+valueWithRect:(NSRect)rect is available on Mac OS X
// NSValue/+valueWithCGRect:(CGRect)rect is available on iOS
// comment/uncomment the corresponding lines depending on which platform you're targeting
// Mac OS X
animation.fromValue = [NSValue valueWithRect:NSRectFromCGRect(oldBounds)];
animation.toValue = [NSValue valueWithRect:NSRectFromCGRect(newBounds)];
// iOS
//animation.fromValue = [NSValue valueWithCGRect:oldBounds];
//animation.toValue = [NSValue valueWithCGRect:newBounds];
// Update the layer's bounds so the layer doesn't snap back when the animation completes.
layer.bounds = newBounds;
// Add the animation, overriding the implicit animation.
[layer addAnimation:animation forKey:#"bounds"];
}
You can combine these animations using a CAAnimationGroup if you need to move and resize a layer at the same time.
we can change the properties of "bounds" and "position" to animate it, such as
-(void)handleTap2:(UITapGestureRecognizer *)recognizer {
UIImageView *vw = (UIImageView *)[recognizer view];
CGPoint startPoint = CGPointMake(vw.frame.size.width/2+vw.frame.origin.x, vw.frame.size.height/2+vw.frame.origin.y);
CGPoint endPoint = CGPointMake(160, 240);
CGRect startBounds = vw.bounds;
CGRect stopBounds = self.view.bounds;
layer = [CALayer layer];
layer.frame = self.view.frame;
layer.contents = (id)[vw.image CGImage];
[self.view.window.layer addSublayer:layer];
CABasicAnimation * baseAnimation = [CABasicAnimation animationWithKeyPath:#"position"];
baseAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
baseAnimation.fromValue = [NSValue valueWithCGPoint:startPoint] ;
baseAnimation.toValue = [NSValue valueWithCGPoint:endPoint] ;
CABasicAnimation * boundsAnimation = [CABasicAnimation animationWithKeyPath:#"bounds"];
boundsAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
boundsAnimation.fromValue = [NSValue valueWithCGRect:startBounds] ; boundsAnimation.toValue = [NSValue valueWithCGRect:stopBounds] ;
CAAnimationGroup * group =[CAAnimationGroup animation];
group.removedOnCompletion=NO; group.fillMode=kCAFillModeForwards;
group.animations =[NSArray arrayWithObjects:baseAnimation, boundsAnimation, nil];
group.duration = 0.7;
[layer addAnimation:group forKey:#"frame"];
}
The question is antique, but I will answer it anyway.
Frame property is not animatable. You have to animate other properties. Also you have to disable implicit animations.
let updatedBounds = ...
let animation = CABasicAnimation(keyPath: "bounds")
animation.duration = 0.5
//it's better to start animation from presentation layer in case there is already animation going on
animation.fromValue = customLayer.presentation()?.bounds
animation.toValue = updatedBounds
customLayer.add(animation, forKey: nil)
//disable implicit animation for thoose properties
CATransaction.begin()
CATransaction.setDisableActions(true)
//update properties so they will be updated at the end of animation
customLayer.bounds = updatedBounds
customLayer.position = originalRect.origin
customLayer.anchorPoint = CGPoint(x: 0, y: 0)
CATransaction.commit()
Extension in swift 4
import UIKit
extension CALayer {
func moveTo(point: CGPoint, animated: Bool) {
if animated {
let animation = CABasicAnimation(keyPath: "position")
animation.fromValue = value(forKey: "position")
animation.toValue = NSValue(cgPoint: point)
animation.fillMode = .forwards
self.position = point
add(animation, forKey: "position")
} else {
self.position = point
}
}
func resize(to size: CGSize, animated: Bool) {
let oldBounds = bounds
var newBounds = oldBounds
newBounds.size = size
if animated {
let animation = CABasicAnimation(keyPath: "bounds")
animation.fromValue = NSValue(cgRect: oldBounds)
animation.toValue = NSValue(cgRect: newBounds)
animation.fillMode = .forwards
self.bounds = newBounds
add(animation, forKey: "bounds")
} else {
self.bounds = newBounds
}
}
func resizeAndMove(frame: CGRect, animated: Bool, duration: TimeInterval = 0) {
if animated {
let positionAnimation = CABasicAnimation(keyPath: "position")
positionAnimation.fromValue = value(forKey: "position")
positionAnimation.toValue = NSValue(cgPoint: CGPoint(x: frame.midX, y: frame.midY))
let oldBounds = bounds
var newBounds = oldBounds
newBounds.size = frame.size
let boundsAnimation = CABasicAnimation(keyPath: "bounds")
boundsAnimation.fromValue = NSValue(cgRect: oldBounds)
boundsAnimation.toValue = NSValue(cgRect: newBounds)
let groupAnimation = CAAnimationGroup()
groupAnimation.animations = [positionAnimation, boundsAnimation]
groupAnimation.fillMode = .forwards
groupAnimation.duration = duration
groupAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
self.frame = frame
add(groupAnimation, forKey: "frame")
} else {
self.frame = frame
}
}
}
Here's a simple, fully working, example which may help someone.
Just call .slideUp() on the class and it will slide up.
class Slidey: YourViewClass {
func slideUp() {
print("\n\n SLIDE")
let FF = layer.position
var TT = FF
TT.y -= 100
print(FF)
print(TT)
CATransaction.begin()
CATransaction.setDisableActions(true)
CATransaction.setCompletionBlock{ [weak self] in
print("DONE")
}
let a = CABasicAnimation(keyPath: "position")
a.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
a.isCumulative = false
a.autoreverses = false
a.isRemovedOnCompletion = true
a.repeatCount = 0
a.fromValue = FF
a.toValue = TT
a.duration = 0.70
layer.add(a, forKey: nil)
CATransaction.commit()
}
}
I guess you need to change your last line to make it work:
[myLayer addAnimation:frameAnimation forKey:#"frame"];
You may also set an action to the layer to make all frame changes animated with your animation:
CABasicAnimation *frameAnimation = [CABasicAnimation animation];
frameAnimation.duration = 2.5;
frameAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
myLayer.actions = [NSDictionary dictionaryWithObjectsAndKeys:frameAnimation, #"frame", nil];
In CALayer's actionForKey: method reference you can find how layer looks up for the actions to animated its properties.

how to create iphone's wobbling icon effect?

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!
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.)
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
];
}
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];
}
I have written a sample app that attempts to replicate the home screen wobble and icon movement: iPhone Sample Code: Tiles
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)
}
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.
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"];
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)
}
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
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)
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:)];
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()
}
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()
}