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'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.
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
}
#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 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()
}
}
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"];
}
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];
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
}
Check out the openspringboard project.
In particular, setIconAnimation:(BOOL)isAnimating in OpenSpringBoard.m. That should give you some ideas on how to do this.
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)];
}
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()
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.
Related
I am currently making an application that uses a custom View Controller container. Multiple views are on the screen at one time and when one is tapped, the selected view controller animates to full screen. In doing so, the selected view controllers subviews scale as well (frame, font size, etc.) Though, UILabel's font property is not animatable leading to issues. I have tried multiple solutions but all flat out suck.
The solutions I have tried are:
Take a screenshot of the larger view and animating the change (similar to how Flipboard does)
Animate by using the transform property
Zooming out a UIScrollView and zooming it in when brought to full screen.
Setting adjustsFontSizeToFitWidth to YES and setting the fontSize prior to animation
One has been the best solution so far but I am not satisfied with it.
I'm looking for other suggestions if anyone has any or a UILabel substitue that animates smoothly using [UIView animate..].
Here is a good example that is similar to what I would like my UILabel to do:
http://www.cocoawithlove.com/2010/09/zoomingviewcontroller-to-animate-uiview.html
EDIT: This code works
// Load View
self.label = [[UILabel alloc] init];
self.label.text = #"TEXT";
self.label.font = [UIFont boldSystemFontOfSize:20.0];
self.label.backgroundColor = [UIColor clearColor];
[self.label sizeToFit];
[self.view addSubview:self.label];
// Animation
self.label.font = [UIFont boldSystemFontOfSize:80.0];
self.label.transform = CGAffineTransformScale(self.label.transform, .25, .25);
[self.label sizeToFit];
[UIView animateWithDuration:1.0 animations:^{
self.label.transform = CGAffineTransformScale(self.label.transform, 4.0, 4.0);
self.label.center = self.view.center;
} completion:^(BOOL finished) {
self.label.font = [UIFont boldSystemFontOfSize:80.0];
self.label.transform = CGAffineTransformScale(self.label.transform, 1.0, 1.0);
[self.label sizeToFit];
}];
You can change the size and font of your UILabel with animation like below .. here I just put the example of how to change the font of UILabel with transform Animation ..
yourLabel.font = [UIFont boldSystemFontOfSize:35]; // set font size which you want instead of 35
yourLabel.transform = CGAffineTransformScale(yourLabel.transform, 0.35, 0.35);
[UIView animateWithDuration:1.0 animations:^{
yourLabel.transform = CGAffineTransformScale(yourLabel.transform, 5, 5);
}];
For 2017 onwards....
Swift 3.0, 4.0
UIView.animate(withDuration: 0.5) {
label.transform = CGAffineTransform(scaleX: 1.1, y: 1.1) //Scale label area
}
Critical:
The critical point to avoid blurring is you must begin with the biggest size, and shrink it. Then expand to "1" when needed.
For quick "pops" (like a highlight animation) it's OK to expand beyond 1 but if you are transitioning between two sizes, make the larger size the "correct" normal one.
I've created UILabel extension in Swift.
import UIKit
extension UILabel {
func animate(font: UIFont, duration: TimeInterval) {
// let oldFrame = frame
let labelScale = self.font.pointSize / font.pointSize
self.font = font
let oldTransform = transform
transform = transform.scaledBy(x: labelScale, y: labelScale)
// let newOrigin = frame.origin
// frame.origin = oldFrame.origin // only for left aligned text
// frame.origin = CGPoint(x: oldFrame.origin.x + oldFrame.width - frame.width, y: oldFrame.origin.y) // only for right aligned text
setNeedsUpdateConstraints()
UIView.animate(withDuration: duration) {
//L self.frame.origin = newOrigin
self.transform = oldTransform
self.layoutIfNeeded()
}
}
}
Uncomment lines if the label text is left or right aligned.
You could also use CATextLayer which has fontSize as an animatable property.
let startFontSize: CGFloat = 20
let endFontSize: CGFloat = 80
let textLayer = CATextLayer()
textLayer.string = "yourText"
textLayer.font = yourLabel.font.fontName as CFTypeRef?
textLayer.fontSize = startFontSize
textLayer.foregroundColor = UIColor.black.cgColor
textLayer.contentsScale = UIScreen.main.scale //for some reason CATextLayer by default only works for 1x screen resolution and needs this line to work properly on 2x, 3x, etc. ...
textLayer.frame = parentView.bounds
parentView.layer.addSublayer(textLayer)
//animation:
let duration: TimeInterval = 1
textLayer.fontSize = endFontSize //because upon completion of the animation CABasicAnimation resets the animated CALayer to its original state (as opposed to changing its properties to the end state of the animation), setting fontSize to endFontSize right BEFORE the animation starts ensures the fontSize doesn't jump back right after the animation.
let fontSizeAnimation = CABasicAnimation(keyPath: "fontSize")
fontSizeAnimation.fromValue = startFontSize
fontSizeAnimation.toValue = endFontSize
fontSizeAnimation.duration = duration
fontSizeAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
textLayer.add(fontSizeAnimation, forKey: nil)
I used it in my project: https://github.com/yinanq/AngelListJobs
This animation keeps the font top left aligned (unlike CGAffineTransformScale scaling the label from center), pro or con depending on your needs. A disadvantage of CATextLayer is that CALayers don't work with autolayout constraint animation (which I happened to need and solved it by making a UIView containing just the CATextLayer and animating its constraints).
For those not looking for a transform, but actual value change:
UIView.transition(with: label, duration: 0.25, options: .transitionCrossDissolve, animations: {
self.label.font = UIFont.systemFont(ofSize: 15)
}) { isFinished in }
For someone who wants to adjust direction of animation
I have created an extension for UILabel to animate font size change
extension UILabel {
func animate(fontSize: CGFloat, duration: TimeInterval) {
let startTransform = transform
let oldFrame = frame
var newFrame = oldFrame
let scaleRatio = fontSize / font.pointSize
newFrame.size.width *= scaleRatio
newFrame.size.height *= scaleRatio
newFrame.origin.x = oldFrame.origin.x - (newFrame.size.width - oldFrame.size.width) * 0.5
newFrame.origin.y = oldFrame.origin.y - (newFrame.size.height - oldFrame.size.height) * 0.5
frame = newFrame
font = font.withSize(fontSize)
transform = CGAffineTransform.init(scaleX: 1 / scaleRatio, y: 1 / scaleRatio);
layoutIfNeeded()
UIView.animate(withDuration: duration, animations: {
self.transform = startTransform
newFrame = self.frame
}) { (Bool) in
self.frame = newFrame
}
}
If you want to adjust direction of animation, use below method and put a suitable anchor point.
SWIFT
struct LabelAnimateAnchorPoint {
// You can add more suitable archon point for your needs
static let leadingCenterY = CGPoint.init(x: 0, y: 0.5)
static let trailingCenterY = CGPoint.init(x: 1, y: 0.5)
static let centerXCenterY = CGPoint.init(x: 0.5, y: 0.5)
static let leadingTop = CGPoint.init(x: 0, y: 0)
}
extension UILabel {
func animate(fontSize: CGFloat, duration: TimeInterval, animateAnchorPoint: CGPoint) {
let startTransform = transform
let oldFrame = frame
var newFrame = oldFrame
let archorPoint = layer.anchorPoint
let scaleRatio = fontSize / font.pointSize
layer.anchorPoint = animateAnchorPoint
newFrame.size.width *= scaleRatio
newFrame.size.height *= scaleRatio
newFrame.origin.x = oldFrame.origin.x - (newFrame.size.width - oldFrame.size.width) * animateAnchorPoint.x
newFrame.origin.y = oldFrame.origin.y - (newFrame.size.height - oldFrame.size.height) * animateAnchorPoint.y
frame = newFrame
font = font.withSize(fontSize)
transform = CGAffineTransform.init(scaleX: 1 / scaleRatio, y: 1 / scaleRatio);
layoutIfNeeded()
UIView.animate(withDuration: duration, animations: {
self.transform = startTransform
newFrame = self.frame
}) { (Bool) in
self.layer.anchorPoint = archorPoint
self.frame = newFrame
}
}
}
OBJECTIVE-C
// You can add more suitable archon point for your needs
#define kLeadingCenterYAnchorPoint CGPointMake(0.f, .5f)
#define kTrailingCenterYAnchorPoint CGPointMake(1.f, .5f)
#define kCenterXCenterYAnchorPoint CGPointMake(.5f, .5f)
#define kLeadingTopAnchorPoint CGPointMake(0.f, 0.f)
#implementation UILabel (FontSizeAnimating)
- (void)animateWithFontSize:(CGFloat)fontSize duration:(NSTimeInterval)duration animateAnchorPoint:(CGPoint)animateAnchorPoint {
CGAffineTransform startTransform = self.transform;
CGRect oldFrame = self.frame;
__block CGRect newFrame = oldFrame;
CGPoint archorPoint = self.layer.anchorPoint;
CGFloat scaleRatio = fontSize / self.font.pointSize;
self.layer.anchorPoint = animateAnchorPoint;
newFrame.size.width *= scaleRatio;
newFrame.size.height *= scaleRatio;
newFrame.origin.x = oldFrame.origin.x - (newFrame.size.width - oldFrame.size.width) * animateAnchorPoint.x;
newFrame.origin.y = oldFrame.origin.y - (newFrame.size.height - oldFrame.size.height) * animateAnchorPoint.y;
self.frame = newFrame;
self.font = [self.font fontWithSize:fontSize];
self.transform = CGAffineTransformScale(self.transform, 1.f / scaleRatio, 1.f / scaleRatio);
[self layoutIfNeeded];
[UIView animateWithDuration:duration animations:^{
self.transform = startTransform;
newFrame = self.frame;
} completion:^(BOOL finished) {
self.layer.anchorPoint = archorPoint;
self.frame = newFrame;
}];
}
#end
For example, to animate changing label font size to 30, duration 1s from center and scale bigger. Simply call
SWIFT
YOUR_LABEL.animate(fontSize: 30, duration: 1, animateAnchorPoint: LabelAnimateAnchorPoint.centerXCenterY)
OBJECTIVE-C
[YOUR_LABEL animateWithFontSize:30
duration:1
animateAnchorPoint:kCenterXCenterYAnchorPoint];
Swift 3.0 & Swift 4.0
UIView.animate(withDuration: 0.5, delay: 0.1, options: .curveLinear, animations: {
label.transform = label.transform.scaledBy(x:4,y:4) //Change x,y to get your desired effect.
} ) { (completed) in
//Animation Completed
}
I found each of the suggestions here inadequate for these reasons:
They don't actually change the font size.
They don't play well with frame sizing & auto layout.
Their interface is non-trivial and/or doesn't play nice inside animation blocks.
In order to retain all of these features & still get a smooth animation transition I've combined the transform approach and the font approach.
The interface is simple. Just update the fontSize property and you'll update the font's size. Do this inside an animation block and it'll animate.
#interface UILabel(MPFontSize)
#property(nonatomic) CGFloat fontSize;
#end
As for the implementation, there's the simple way, and there's the better way.
Simple:
#implementation UILabel(MPFontSize)
- (void)setFontSize:(CGFloat)fontSize {
CGAffineTransform originalTransform = self.transform;
UIFont *targetFont = [self.font fontWithSize:fontSize];
[UIView animateWithDuration:0 delay:0 options:0 animations:^{
self.transform = CGAffineTransformScale( originalTransform,
fontSize / self.fontSize, fontSize / self.fontSize );
} completion:^(BOOL finished) {
self.transform = originalTransform;
if (finished)
self.font = targetFont;
}];
}
- (CGFloat)fontSize {
return self.font.pointSize;
};
#end
Now, the problem with this is that the layout can stutter upon completion, because the view's frame is sized based on the original font all the way until the animation completion, at which point the frame updates to accommodate the target font without animation.
Fixing this problem is a little harder because we need to override intrinsicContentSize. You can do this either by subclassing UILabel or by swizzling the method. I personally swizzle the method, because it lets me keep a generic fontSize property available to all UILabels, but that depends on some library code I can't share here. Here is how you would go about this using subclassing.
Interface:
#interface AnimatableLabel : UILabel
#property(nonatomic) CGFloat fontSize;
#end
Implementation:
#interface AnimatableLabel()
#property(nonatomic) UIFont *targetFont;
#property(nonatomic) UIFont *originalFont;
#end
#implementation AnimatableLabel
- (void)setFontSize:(CGFloat)fontSize {
CGAffineTransform originalTransform = self.transform;
self.originalFont = self.font;
self.targetFont = [self.font fontWithSize:fontSize];
[self invalidateIntrinsicContentSize];
[UIView animateWithDuration:0 delay:0 options:0 animations:^{
self.transform = CGAffineTransformScale( originalTransform,
fontSize / self.fontSize, fontSize / self.fontSize );
} completion:^(BOOL finished) {
self.transform = originalTransform;
if (self.targetFont) {
if (finished)
self.font = self.targetFont;
self.targetFont = self.originalFont = nil;
[self invalidateIntrinsicContentSize];
}
}];
}
- (CGFloat)fontSize {
return self.font.pointSize;
};
- (CGSize)intrinsicContentSize {
#try {
if (self.targetFont)
self.font = self.targetFont;
return self.intrinsicContentSize;
}
#finally {
if (self.originalFont)
self.font = self.originalFont;
}
}
#end
If you want to animate the text size from another anchor point, here is the Swift 5 solution:
How to apply:
yourLabel.setAnimatedFont(.systemFont(ofSize: 48), duration: 0.2, anchorPointX: 0, anchorPointY: 1)
Extensions:
extension UILabel {
/// Animate font size from a given anchor point of the label.
/// - Parameters:
/// - duration: Animation measured in seconds
/// - anchorPointX: 0 = left, 0.5 = center, 1 = right
/// - anchorPointY: 0 = top, 0.5 = center, 1 = bottom
func setAnimatedFont(_ font: UIFont, duration: TimeInterval, anchorPointX: CGFloat, anchorPointY: CGFloat) {
guard let oldFont = self.font else { return }
setAnchorPoint(CGPoint(x: anchorPointX, y: anchorPointY))
self.font = font
let scaleFactor = oldFont.pointSize / font.pointSize
let oldTransform = transform
transform = transform.scaledBy(x: scaleFactor, y: scaleFactor)
setNeedsUpdateConstraints()
UIView.animate(withDuration: duration) {
self.transform = oldTransform
self.layoutIfNeeded()
}
}
}
extension UIView {
/// Change the anchor point without moving the view's position.
/// - Parameters:
/// - point: The layer's bounds rectangle.
func setAnchorPoint(_ point: CGPoint) {
let oldOrigin = frame.origin
layer.anchorPoint = point
let newOrigin = frame.origin
let translation = CGPoint(x: newOrigin.x - oldOrigin.x, y: newOrigin.y - oldOrigin.y)
translatesAutoresizingMaskIntoConstraints = true
center = CGPoint(x: center.x - translation.x, y: center.y - translation.y)
}
}
how can I add an animation to a UITextField to indicate wrong password exactly like the one in facebook app (at login screen) or the Mac OS X login box ?
thank you in advance.
Something like that
-(void)shake:(UIView *)theOneYouWannaShake
{
[UIView animateWithDuration:0.03 animations:^
{
theOneYouWannaShake.transform = CGAffineTransformMakeTranslation(5*direction, 0);
}
completion:^(BOOL finished)
{
if(shakes >= 10)
{
theOneYouWannaShake.transform = CGAffineTransformIdentity;
return;
}
shakes++;
direction = direction * -1;
[self shake:theOneYouWannaShake];
}];
}
So you need three more things: An int direction which is set to 1 before the shake is called an int shakes, which is set to 0 before the shake is called and a constant MAX_SHAKES which is as large as you like. Hope that helps.
EDIT:
call it like this:
direction = 1;
shakes = 0;
[self shake:aUIView];
inside header file add
int direction;
int shakes;
(Jan 16 2015) Update:
(enum UIViewAnimationOptions) cast is fine and UIViewAnimationOptionCurveEaseOut is 2 << 16 per UIView.h under
typedef NS_OPTIONS(NSUInteger, UIViewAnimationOptions)
(Jan 31 2013)
Further modified Kai's answer to include:
edge delay of 0.01s
easeInOut
reduce duration of shakes every shake from 0.09 to 0.04
throttle down movement by a pt every 1 complete loop (right-left-right)
Note: if you plan on shaking two controls (email and password) together you might want to avoid using class or static variables for shakes and translate. Instead, initialize and pass shake and translate as parameters. I used statics so no class variables needed.
-(void)shakeAnimation:(UIView*) view {
const int reset = 5;
const int maxShakes = 6;
//pass these as variables instead of statics or class variables if shaking two controls simultaneously
static int shakes = 0;
static int translate = reset;
[UIView animateWithDuration:0.09-(shakes*.01) // reduce duration every shake from .09 to .04
delay:0.01f//edge wait delay
options:(enum UIViewAnimationOptions) UIViewAnimationCurveEaseInOut
animations:^{view.transform = CGAffineTransformMakeTranslation(translate, 0);}
completion:^(BOOL finished){
if(shakes < maxShakes){
shakes++;
//throttle down movement
if (translate>0)
translate--;
//change direction
translate*=-1;
[self shakeAnimation:view];
} else {
view.transform = CGAffineTransformIdentity;
shakes = 0;//ready for next time
translate = reset;//ready for next time
return;
}
}];
}
This Swift 2.0 answer requires no recursion and no loops. Just leverages CABasicAnimation by refining this SO answer:
func shakeView(shakeView: UIView) {
let shake = CABasicAnimation(keyPath: "position")
let xDelta = CGFloat(5)
shake.duration = 0.15
shake.repeatCount = 2
shake.autoreverses = true
let from_point = CGPointMake(shakeView.center.x - xDelta, shakeView.center.y)
let from_value = NSValue(CGPoint: from_point)
let to_point = CGPointMake(shakeView.center.x + xDelta, shakeView.center.y)
let to_value = NSValue(CGPoint: to_point)
shake.fromValue = from_value
shake.toValue = to_value
shake.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
shakeView.layer.addAnimation(shake, forKey: "position")
}
Updated for Swift 4:
func shakeView(_ shakeView: UIView) {
let shake = CABasicAnimation(keyPath: "position")
let xDelta = CGFloat(5)
shake.duration = 0.15
shake.repeatCount = 2
shake.autoreverses = true
let from_point = CGPoint(x: shakeView.center.x - xDelta, y: shakeView.center.y)
let from_value = NSValue(cgPoint: from_point)
let to_point = CGPoint(x: shakeView.center.x + xDelta, y: shakeView.center.y)
let to_value = NSValue(cgPoint: to_point)
shake.fromValue = from_value
shake.toValue = to_value
shake.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
shakeView.layer.add(shake, forKey: "position")
}
Based on a previous answer as swift method ready to use :
func shakeTextField(textField: UITextField, numberOfShakes: Int, direction: CGFloat, maxShakes: Int) {
let interval: TimeInterval = 0.03
UIView.animate(withDuration: interval, animations: { () -> Void in
textField.transform = CGAffineTransform(translationX: 5 * direction, y: 0)
}, completion: { (aBool :Bool) -> Void in
if (numberOfShakes >= maxShakes) {
textField.transform = .identity
textField.becomeFirstResponder()
return
}
self.shakeTextField(textField: textField, numberOfShakes: numberOfShakes + 1, direction: direction * -1, maxShakes: maxShakes)
})
}
To call it :
shakeTextField(aTextField,numberOfShakes:0, direction :1, maxShakes : 10)
If you came here looking for a MonoTouch answer, here is a rough translation of Dickey's code:
public static void /*Harlem*/Shake (this UIView view, int shakes = 6, int translation = 5)
{
UIView.Animate (0.03 + (shakes * 0.01), 0.01, UIViewAnimationOptions.CurveEaseInOut, () => {
view.Transform = CGAffineTransform.MakeTranslation (translation, 0);
}, () => {
if (shakes == 0) {
view.Transform = CGAffineTransform.MakeIdentity ();
return;
}
if (translation > 0)
translation --;
translation *= -1;
shakes --;
Shake (view, shakes, translation);
});
}
Put it with the rest of your extensions methods and call like that:
password.Shake ();
I created a category method for UIView that can be used to shake any element - e.g. a UITextField - with the ability to get notified after the shaking has ended. Here is how to use it:
[myPasswordField shake];
// Or with a callback after the shake
[myPasswordField shakeWithCallback:^{
NSLog(#"Shaking has ended");
}];
Here is the code.
UIView+Shake.h
#import <UIKit/UIKit.h>
#interface UIView (UIView_Shake)
-(void)shake;
-(void)shakeWithCallback:(void (^)(void))completeBlock;
#end
UIView+Shake.m
#import "UIView+Shake.h"
#import <objc/runtime.h>
#implementation UIView (UIView_Shake)
static void *NumCurrentShakesKey;
static void *NumTotalShakesKey;
static void *ShakeDirectionKey;
- (int)numCurrentShakes {
return [objc_getAssociatedObject(self, &NumCurrentShakesKey) intValue];
}
- (void)setNumCurrentShakes:(int)value {
objc_setAssociatedObject(self, &NumCurrentShakesKey, [NSNumber numberWithInt:value], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (int)numTotalShakes {
return [objc_getAssociatedObject(self, &NumTotalShakesKey) intValue];
}
- (void)setNumTotalShakes:(int)value {
objc_setAssociatedObject(self, &NumTotalShakesKey, [NSNumber numberWithInt:value], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (int)shakeDirection {
return [objc_getAssociatedObject(self, &ShakeDirectionKey) intValue];
}
- (void)setShakeDirection:(int)value {
objc_setAssociatedObject(self, &ShakeDirectionKey, [NSNumber numberWithInt:value], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(void)shake {
[self shakeNextWithCompleteBlock:nil];
}
-(void)shakeWithCallback:(void (^)(void))completeBlock {
self.numCurrentShakes = 0;
self.numTotalShakes = 6;
self.shakeDirection = 8;
[self shakeNextWithCompleteBlock:completeBlock];
}
-(void)shakeNextWithCompleteBlock:(void (^)(void))completeBlock
{
UIView* viewToShake = self;
[UIView animateWithDuration:0.08
animations:^
{
viewToShake.transform = CGAffineTransformMakeTranslation(self.shakeDirection, 0);
}
completion:^(BOOL finished)
{
if(self.numCurrentShakes >= self.numTotalShakes)
{
viewToShake.transform = CGAffineTransformIdentity;
if(completeBlock != nil) {
completeBlock();
}
return;
}
self.numCurrentShakes++;
self.shakeDirection = self.shakeDirection * -1;
[self shakeNextWithCompleteBlock:completeBlock];
}];
}
#end
Here's my spin on it:
#implementation UITextField (Shake)
- (void)shake {
[self shakeWithIterations:0 direction:1 size:4];
}
#pragma mark - Private
- (void)shakeWithIterations:(int)iterations direction:(int)direction size:(int)size {
[UIView animateWithDuration:0.09-(iterations*.01) animations:^{
self.transform = CGAffineTransformMakeTranslation(size*direction, 0);
} completion:^(BOOL finished) {
if (iterations >= 5 || size <= 0) {
self.transform = CGAffineTransformIdentity;
return;
}
[self shakeWithIterations:iterations+1 direction:direction*-1 size:MAX(0, size-1)];
}];
}
#end
I tried #stefreak solution but the loop approach doesn't work on iOS 7.1. So I combined the solutions from #stefreak and #Chris, and added the completion block to be notified when the shaking finishes. Here is my code:
- (void)shakeView:(UIView *)view iterations:(NSInteger)iterations direction:(NSInteger)direction completion:(void (^)())completion
{
const NSInteger MAX_SHAKES = 6;
const CGFloat SHAKE_DURATION = 0.05;
const CGFloat SHAKE_TRANSFORM = 10.0;
[UIView animateWithDuration:SHAKE_DURATION
delay:0.0
options:UIViewAnimationOptionCurveEaseIn
animations:^{
view.transform = iterations >= MAX_SHAKES ? CGAffineTransformIdentity : CGAffineTransformMakeTranslation(SHAKE_TRANSFORM * direction, 0);
} completion:^(BOOL finished) {
if (finished)
{
if (iterations >= MAX_SHAKES)
{
if (completion)
{
completion();
}
}
else
{
[self shakeView:view iterations:(iterations + 1) direction:(direction * -1) completion:completion];
}
}
}];
}
- (void)shakeView:(UIView *)view completion:(void (^)())completion
{
[self shakeView:view iterations:0 direction:1 completion:completion];
}
You can also do it using basic animation
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.09
animation.repeatCount = 4
animation.autoreverses = true
animation.fromValue = NSValue(CGPoint: CGPointMake(txtField.center.x - 10, txtField.center.y))
animation.toValue = NSValue(CGPoint: CGPointMake(txtField.center.x + 10, txtField.center.y))
txtField.layer.addAnimation(animation, forKey: "position")
Here you can the change duration,repeatCount.Changing into the fromValue and toValue will change distance moved in the shake
Since the question was about Objective-C, and since I am using Objective-C in my project, I think this Objective-C translation of this previous Swift answer could be useful to someone else:
- (void)shakeView:(UIView*)view
{
CABasicAnimation *shake = [CABasicAnimation animationWithKeyPath:#"position"];
CGFloat xDelta = 5.0;
shake.duration = 0.15;
shake.repeatCount = 2;
shake.autoreverses = YES;
CGPoint fromPoint = CGPointMake(view.center.x - xDelta, view.center.y);
CGPoint toPoint = CGPointMake(view.center.x + xDelta, view.center.y);
shake.fromValue = [NSValue valueWithCGPoint:fromPoint];
shake.toValue = [NSValue valueWithCGPoint:toPoint];
shake.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[view.layer addAnimation:shake forKey:#"position"];
}
There's a Swift Library for animating Textfield in github here. Simply import the swift file and implement as below
// Shake with the default speed
self.textField.shake(10, delta:5) //10 no. of shakes with 5 points wide
// Shake with a custom speed
self.sampleText.shake(10, delta: 5, speed: 0.10) //10 no. of shakes with 5 points wide in 100ms per shake
Swift 3 and stack_view instaed textField
func shakeTextField (stack_view : UIStackView, numberOfShakes : Int, direction: CGFloat, maxShakes : Int) {
let interval : TimeInterval = 0.05
UIView.animate(withDuration: interval, animations: { () -> Void in
stack_view.transform = CGAffineTransform(translationX: 5 * direction, y: 0)
}, completion: { (aBool :Bool) -> Void in
if (numberOfShakes >= maxShakes) {
stack_view.becomeFirstResponder()
return
}
self.shakeTextField(stack_view: stack_view, numberOfShakes: numberOfShakes + 1, direction: direction * -1, maxShakes: maxShakes )
})
}
If I understand correctly scaling a UIView with CGAffineTransform anchors the transformation to its center.
In particular:
self.frame = CGRectMake(0,0,100,100);
self.transform = CGAffineTransformMakeScale(2, 2);
NSLog(#"%f;%f;%f;%f", self.frame.origin.x, self.frame.origin.y, self.frame.size.width, self.frame.size.height);
Prints:
-50;-50;200;200
How do you create a CGAffineTransform scale that uses a specific anchor point (say 0;0)?
(a)
Scale and then translate?
Something like :
CGAffineTransform t = CGAffineTransformMakeScale(2, 2);
t = CGAffineTransformTranslate(t, width/2, height/2);
self.transform = t;
(b)
Set the anchor point (which is probably what you want really)
[self layer].anchorPoint = CGPointMake(0.0f, 0.0f);
self.transform = CGAffineTransformMakeScale(2, 2);
(c)
Set the center again to make sure it's in the same place?
CGPoint center = self.center;
self.transform = CGAffineTransformMakeScale(2, 2);
self.center = center;
Firstly #import <QuartzCore/QuartzCore.h> and then set the anchor points of your view:
[[self layer] setAnchorPoint:CGPointMake(0, 0)];
This is the way I found the scale a view while keeping it's origin in place (0,0) in Swift5
func animate() {
myView.setAnchorPoint(CGPoint(x: 0, y: 0))
myView.transform = CGAffineTransform(translationX: -0.5, y: -0.5)
let animator = UIViewPropertyAnimator(duration: 0.3, curve: .easeOut) {
self.myView.transform = CGAffineTransform(scaleX: 0.25, y: 0.25)
}
}
extension UIView {
func setAnchorPoint(_ point: CGPoint) {
var newPoint = CGPoint(x: bounds.size.width * point.x, y: bounds.size.height * point.y)
var oldPoint = CGPoint(x: bounds.size.width * layer.anchorPoint.x, y: bounds.size.height * layer.anchorPoint.y);
newPoint = newPoint.applying(transform)
oldPoint = oldPoint.applying(transform)
var position = layer.position
position.x -= oldPoint.x
position.x += newPoint.x
position.y -= oldPoint.y
position.y += newPoint.y
layer.position = position
layer.anchorPoint = point
}
}
credit to Hacking with Swift
How can I add a bounce effect when I am about to show an UIImageView as a subview? Do I have to use CoreAnimation to do this? My only guess right now is to use CAKeyframeAnimation, please let me know if there's a better way. Here's my current code:
CABasicAnimation * theAnimation=[CABasicAnimation animationWithKeyPath:#"transform.translation.y"];
theAnimation.delegate = self;
theAnimation.duration = 1.0;
theAnimation.fromValue = [NSNumber numberWithFloat:notif.center.y];
theAnimation.toValue = [NSNumber numberWithFloat:notif.center.y-20];
theAnimation.repeatCount = 3;
y-axis animation using CABasicAnimation:
CGPoint origin = self.imageView.center;
CGPoint target = CGPointMake(self.imageView.center.x, self.imageView.center.y+100);
CABasicAnimation *bounce = [CABasicAnimation animationWithKeyPath:#"position.y"];
bounce.duration = 0.5;
bounce.fromValue = [NSNumber numberWithInt:origin.y];
bounce.toValue = [NSNumber numberWithInt:target.y];
bounce.repeatCount = 2;
bounce.autoreverses = YES;
[self.imageView.layer addAnimation:bounce forKey:#"position"];
If you want to implement shrink and grow you have to add a CGAffineTransformMakeScale, eg:
// grow
CGAffineTransform transform = CGAffineTransformMakeScale(1.3, 1.3);
imageView.transform = transform;
Bouncy (expand/shrink) animation in Swift:
var selected: Bool {
willSet(selected) {
let expandTransform:CGAffineTransform = CGAffineTransformMakeScale(1.2, 1.2);
if (!self.selected && selected) {
self.imageView.image = SNStockCellSelectionAccessoryViewImage(selected)
self.imageView.transform = expandTransform
UIView.animateWithDuration(0.4,
delay:0.0,
usingSpringWithDamping:0.40,
initialSpringVelocity:0.2,
options: .CurveEaseOut,
animations: {
self.imageView.transform = CGAffineTransformInvert(expandTransform)
}, completion: {
//Code to run after animating
(value: Bool) in
})
}
}
}
var imageView:UIImageView
If imageView is correctly added to the view as a subview, toggling between selected = false to selected = true should swap the image with a bouncy animation. SNStockCellSelectionAccessoryViewImage just returns a different image based on the current selection state, see below:
private let SNStockCellSelectionAccessoryViewPlusIconSelected:UIImage = UIImage(named:"PlusIconSelected")!
private let SNStockCellSelectionAccessoryViewPlusIcon:UIImage = UIImage(named:"PlusIcon")!
private func SNStockCellSelectionAccessoryViewImage(selected:Bool) -> UIImage {
return selected ? SNStockCellSelectionAccessoryViewPlusIconSelected : SNStockCellSelectionAccessoryViewPlusIcon
}
The GIF example below is a bit slowed down, the actual animation happens faster:
[UIView animateWithDuration:0.8
delay:0
usingSpringWithDamping:0.5
initialSpringVelocity:0.5
options:(UIViewAnimationOptionAutoreverse|
UIViewAnimationOptionRepeat)
animations:^{
CGRect frame = view.frame;
frame.origin.y -= 8;
view.frame = frame;
} completion:nil];
Play with the values to get different effects.
#Jano's answer in Swift
let origin:CGPoint = self.image.center
let target:CGPoint = CGPointMake(self.image.center.x, self.image.center.y+100)
let bounce = CABasicAnimation(keyPath: "position.y")
bounce.duration = 1
bounce.fromValue = origin.y
bounce.toValue = target.y
bounce.repeatCount = 2
bounce.autoreverses = true
self.image.layer.addAnimation(bounce, forKey: "position")
I followed this link
func imageViewBounceEffect() {
yourImageView.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
UIView.animate(withDuration: 1.35, delay: 0,
usingSpringWithDamping: 0.25,
initialSpringVelocity: 5,
options: .curveEaseOut,
animations: {
self.yourImageView.transform = .identity
})
}
I am developing a commerce application. When I add an item to the shopping cart, I want to create an effect where an image of the item follows a curved path and ends up at the cart tab.
How can I create an animation of an image along a curve like this?
To expand upon what Nikolai said, the best way to handle this is to use Core Animation to animate the motion of the image or view along a Bezier path. This is accomplished using a CAKeyframeAnimation. For example, I've used the following code to animate an image of a view into an icon to indicate saving (as can be seen in the video for this application):
First of all import QuartzCore header file
#import <QuartzCore/QuartzCore.h>
UIImageView *imageViewForAnimation = [[UIImageView alloc] initWithImage:imageToAnimate];
imageViewForAnimation.alpha = 1.0f;
CGRect imageFrame = imageViewForAnimation.frame;
//Your image frame.origin from where the animation need to get start
CGPoint viewOrigin = imageViewForAnimation.frame.origin;
viewOrigin.y = viewOrigin.y + imageFrame.size.height / 2.0f;
viewOrigin.x = viewOrigin.x + imageFrame.size.width / 2.0f;
imageViewForAnimation.frame = imageFrame;
imageViewForAnimation.layer.position = viewOrigin;
[self.view addSubview:imageViewForAnimation];
// Set up fade out effect
CABasicAnimation *fadeOutAnimation = [CABasicAnimation animationWithKeyPath:#"opacity"];
[fadeOutAnimation setToValue:[NSNumber numberWithFloat:0.3]];
fadeOutAnimation.fillMode = kCAFillModeForwards;
fadeOutAnimation.removedOnCompletion = NO;
// Set up scaling
CABasicAnimation *resizeAnimation = [CABasicAnimation animationWithKeyPath:#"bounds.size"];
[resizeAnimation setToValue:[NSValue valueWithCGSize:CGSizeMake(40.0f, imageFrame.size.height * (40.0f / imageFrame.size.width))]];
resizeAnimation.fillMode = kCAFillModeForwards;
resizeAnimation.removedOnCompletion = NO;
// Set up path movement
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
pathAnimation.calculationMode = kCAAnimationPaced;
pathAnimation.fillMode = kCAFillModeForwards;
pathAnimation.removedOnCompletion = NO;
//Setting Endpoint of the animation
CGPoint endPoint = CGPointMake(480.0f - 30.0f, 40.0f);
//to end animation in last tab use
//CGPoint endPoint = CGPointMake( 320-40.0f, 480.0f);
CGMutablePathRef curvedPath = CGPathCreateMutable();
CGPathMoveToPoint(curvedPath, NULL, viewOrigin.x, viewOrigin.y);
CGPathAddCurveToPoint(curvedPath, NULL, endPoint.x, viewOrigin.y, endPoint.x, viewOrigin.y, endPoint.x, endPoint.y);
pathAnimation.path = curvedPath;
CGPathRelease(curvedPath);
CAAnimationGroup *group = [CAAnimationGroup animation];
group.fillMode = kCAFillModeForwards;
group.removedOnCompletion = NO;
[group setAnimations:[NSArray arrayWithObjects:fadeOutAnimation, pathAnimation, resizeAnimation, nil]];
group.duration = 0.7f;
group.delegate = self;
[group setValue:imageViewForAnimation forKey:#"imageViewBeingAnimated"];
[imageViewForAnimation.layer addAnimation:group forKey:#"savingAnimation"];
[imageViewForAnimation release];
The way to animate along CGPath using UIView.animateKeyframes (Swift 4)
private func animateNew() {
let alphaFrom: CGFloat = 1
let alphaTo: CGFloat = 0.3
let sizeFrom = CGSize(width: 40, height: 20)
let sizeTo = CGSize(width: 80, height: 60)
let originFrom = CGPoint(x: 40, y: 40)
let originTo = CGPoint(x: 240, y: 480)
let deltaWidth = sizeTo.width - sizeFrom.width
let deltaHeight = sizeTo.height - sizeFrom.height
let deltaAlpha = alphaTo - alphaFrom
// Setting default values
imageViewNew.alpha = alphaFrom
imageViewNew.frame = CGRect(origin: originFrom, size: sizeFrom)
// CGPath setup for calculating points on curve.
let curvedPath = CGMutablePath()
curvedPath.move(to: originFrom)
curvedPath.addQuadCurve(to: originTo, control: CGPoint(x: originFrom.x, y: originTo.y))
let path = Math.BezierPath(cgPath: curvedPath, approximationIterations: 10)
// Calculating timing parameters
let duration: TimeInterval = 0.7
let numberOfKeyFrames = 16
let curvePoints = Math.Easing.timing(numberOfSteps: numberOfKeyFrames, .easeOutQuad)
UIView.animateKeyframes(withDuration: duration, delay: 0, options: [.calculationModeCubic], animations: {
// Iterating curve points and adding key frames
for point in curvePoints {
let origin = path.point(atPercentOfLength: point.end)
let size = CGSize(width: sizeFrom.width + deltaWidth * point.end,
height: sizeFrom.height + deltaHeight * point.end)
let alpha = alphaFrom + deltaAlpha * point.end
UIView.addKeyframe(withRelativeStartTime: TimeInterval(point.start), relativeDuration: TimeInterval(point.duration)) {
self.imageViewNew.frame = CGRect(origin: origin, size: size)
self.imageViewNew.alpha = alpha
}
}
}, completion: nil)
}
File: Math.Easing.swift
// Inspired by: RBBAnimation/RBBEasingFunction.m: https://github.com/robb/RBBAnimation/blob/master/RBBAnimation/RBBEasingFunction.m
extension Math { public struct Easing { } }
extension Math.Easing {
public enum Algorithm: Int {
case linear, easeInQuad, easeOutQuad, easeInOutQuad
}
#inline(__always)
public static func linear(_ t: CGFloat) -> CGFloat {
return t
}
#inline(__always)
public static func easeInQuad(_ t: CGFloat) -> CGFloat {
return t * t
}
#inline(__always)
public static func easeOutQuad(_ t: CGFloat) -> CGFloat {
return t * (2 - t)
}
#inline(__always)
public static func easeInOutQuad(_ t: CGFloat) -> CGFloat {
if t < 0.5 {
return 2 * t * t
} else {
return -1 + (4 - 2 * t) * t
}
}
}
extension Math.Easing {
public struct Timing {
public let start: CGFloat
public let end: CGFloat
public let duration: CGFloat
init(start: CGFloat, end: CGFloat) {
self.start = start
self.end = end
self.duration = end - start
}
public func multiplying(by: CGFloat) -> Timing {
return Timing(start: start * by, end: end * by)
}
}
public static func process(_ t: CGFloat, _ algorithm: Algorithm) -> CGFloat {
switch algorithm {
case .linear:
return linear(t)
case .easeInQuad:
return easeInQuad(t)
case .easeOutQuad:
return easeOutQuad(t)
case .easeInOutQuad:
return easeInOutQuad(t)
}
}
public static func timing(numberOfSteps: Int, _ algorithm: Algorithm) -> [Timing] {
var result: [Timing] = []
let linearStepSize = 1 / CGFloat(numberOfSteps)
for step in (0 ..< numberOfSteps).reversed() {
let linearValue = CGFloat(step) * linearStepSize
let processedValue = process(linearValue, algorithm) // Always in range 0 ... 1
let lastValue = result.last?.start ?? 1
result.append(Timing(start: processedValue, end: lastValue))
}
result = result.reversed()
return result
}
}
File: Math.BezierPath.swift. Look on this SO answer: https://stackoverflow.com/a/50782971/1418981
You can animate a UIView's center property using a CAKeyframeAnimation. See the CoreAnimation programming guide.
Swift 4 version similar to ObjC example from original response.
class KeyFrameAnimationsViewController: ViewController {
let sampleImage = ImageFactory.image(size: CGSize(width: 160, height: 120), fillColor: .blue)
private lazy var imageView = ImageView(image: sampleImage)
private lazy var actionButton = Button(title: "Animate").autolayoutView()
override func setupUI() {
view.addSubviews(imageView, actionButton)
view.backgroundColor = .gray
}
override func setupLayout() {
LayoutConstraint.withFormat("|-[*]", actionButton).activate()
LayoutConstraint.withFormat("V:|-[*]", actionButton).activate()
}
override func setupHandlers() {
actionButton.setTouchUpInsideHandler { [weak self] in
self?.animate()
}
}
private func animate() {
imageView.alpha = 1
let isRemovedOnCompletion = false
let sizeFrom = CGSize(width: 40, height: 20)
let sizeTo = CGSize(width: 80, height: 60)
let originFrom = CGPoint(x: 40, y: 40)
let originTo = CGPoint(x: 240, y: 480)
imageView.frame = CGRect(origin: originFrom, size: sizeFrom)
imageView.layer.position = originFrom
// Set up fade out effect
let fadeOutAnimation = CABasicAnimation(keyPath: "opacity")
fadeOutAnimation.toValue = 0.3
fadeOutAnimation.fillMode = kCAFillModeForwards
fadeOutAnimation.isRemovedOnCompletion = isRemovedOnCompletion
// Set up scaling
let resizeAnimation = CABasicAnimation(keyPath: "bounds.size")
resizeAnimation.toValue = sizeTo
resizeAnimation.fillMode = kCAFillModeForwards
resizeAnimation.isRemovedOnCompletion = isRemovedOnCompletion
// Set up path movement
let pathAnimation = CAKeyframeAnimation(keyPath: "position")
pathAnimation.calculationMode = kCAAnimationPaced;
pathAnimation.fillMode = kCAFillModeForwards;
pathAnimation.isRemovedOnCompletion = isRemovedOnCompletion
// Setting Endpoint of the animation to end animation in last tab use
let curvedPath = CGMutablePath()
curvedPath.move(to: originFrom)
// About curves: https://www.bignerdranch.com/blog/core-graphics-part-4-a-path-a-path/
curvedPath.addQuadCurve(to: originTo, control: CGPoint(x: originFrom.x, y: originTo.y))
pathAnimation.path = curvedPath
let group = CAAnimationGroup()
group.fillMode = kCAFillModeForwards
group.isRemovedOnCompletion = isRemovedOnCompletion
group.animations = [fadeOutAnimation, pathAnimation, resizeAnimation]
group.duration = 0.7
group.setValue(imageView, forKey: "imageViewBeingAnimated")
imageView.layer.add(group, forKey: "savingAnimation")
}
}