Animating UILabel Font Size Change - iphone

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)
}
}

Related

Keep zoomable image in center of UIScrollView

In my iPhone app, I need to provide the user with an ability to zoom/pan a large-ish image on the screen. This is quite simple: I use UIScrollView, set max/min scale factors and zooming/panning works as expected. Here's where things get interesting. The image is a dynamic one, received from a server. It can have any dimensions. When the image first loads, it's scaled down (if needed) to fit completely into the UIScrollView and is centered in the scroll view - the screenshot is below:
Because the proportions of the image are different from those of the scroll view, there's white space added above and below the image so that the image is centered. However when I start zooming the image, the actual image becomes large enough to fill the whole of the scrollview viewport, therefore white paddings at top/bottom are not needed anymore, however they remain there, as can be seen from this screenshot:
I believe this is due to the fact that the UIImageView containing the image is automatically sized to fill the whole of UIScrollView and when zoomed, it just grows proportionally. It has scale mode set to Aspect Fit. UIScrollView's delegate viewForZoomingInScrollView simply returns the image view.
I attempted to recalculate and re-set UIScrollView, contentSize and image view's size in scrollViewDidEndZooming method:
CGSize imgViewSize = imageView.frame.size;
CGSize imageSize = imageView.image.size;
CGSize realImgSize;
if(imageSize.width / imageSize.height > imgViewSize.width / imgViewSize.height) {
realImgSize = CGSizeMake(imgViewSize.width, imgViewSize.width / imageSize.width * imageSize.height);
}
else {
realImgSize = CGSizeMake(imgViewSize.height / imageSize.height * imageSize.width, imgViewSize.height);
}
scrollView.contentSize = realImgSize;
CGRect fr = CGRectMake(0, 0, 0, 0);
fr.size = realImgSize;
imageView.frame = fr;
However this was only making things worse (with bounds still being there but panning not working in the vertical direction).
Is there any way to automatically reduce that whitespace as it becomes unneeded and then increment again during zoom-in? I suspect the work will need to be done in scrollViewDidEndZooming, but I'm not too sure what that code needs to be.
Awesome!
Thanks for the code :)
Just thought I'd add to this as I changed it slightly to improve the behaviour.
// make the change during scrollViewDidScroll instead of didEndScrolling...
-(void)scrollViewDidZoom:(UIScrollView *)scrollView
{
CGSize imgViewSize = self.imageView.frame.size;
CGSize imageSize = self.imageView.image.size;
CGSize realImgSize;
if(imageSize.width / imageSize.height > imgViewSize.width / imgViewSize.height) {
realImgSize = CGSizeMake(imgViewSize.width, imgViewSize.width / imageSize.width * imageSize.height);
}
else {
realImgSize = CGSizeMake(imgViewSize.height / imageSize.height * imageSize.width, imgViewSize.height);
}
CGRect fr = CGRectMake(0, 0, 0, 0);
fr.size = realImgSize;
self.imageView.frame = fr;
CGSize scrSize = scrollView.frame.size;
float offx = (scrSize.width > realImgSize.width ? (scrSize.width - realImgSize.width) / 2 : 0);
float offy = (scrSize.height > realImgSize.height ? (scrSize.height - realImgSize.height) / 2 : 0);
// don't animate the change.
scrollView.contentInset = UIEdgeInsetsMake(offy, offx, offy, offx);
}
Here's my solution that works universally with any tab bar or navigation bar combination or w/o both, translucent or not.
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
// The scroll view has zoomed, so you need to re-center the contents
CGSize scrollViewSize = [self scrollViewVisibleSize];
// First assume that image center coincides with the contents box center.
// This is correct when the image is bigger than scrollView due to zoom
CGPoint imageCenter = CGPointMake(self.scrollView.contentSize.width/2.0,
self.scrollView.contentSize.height/2.0);
CGPoint scrollViewCenter = [self scrollViewCenter];
//if image is smaller than the scrollView visible size - fix the image center accordingly
if (self.scrollView.contentSize.width < scrollViewSize.width) {
imageCenter.x = scrollViewCenter.x;
}
if (self.scrollView.contentSize.height < scrollViewSize.height) {
imageCenter.y = scrollViewCenter.y;
}
self.imageView.center = imageCenter;
}
//return the scroll view center
- (CGPoint)scrollViewCenter {
CGSize scrollViewSize = [self scrollViewVisibleSize];
return CGPointMake(scrollViewSize.width/2.0, scrollViewSize.height/2.0);
}
// Return scrollview size without the area overlapping with tab and nav bar.
- (CGSize) scrollViewVisibleSize {
UIEdgeInsets contentInset = self.scrollView.contentInset;
CGSize scrollViewSize = CGRectStandardize(self.scrollView.bounds).size;
CGFloat width = scrollViewSize.width - contentInset.left - contentInset.right;
CGFloat height = scrollViewSize.height - contentInset.top - contentInset.bottom;
return CGSizeMake(width, height);
}
Swift 5:
public func scrollViewDidZoom(_ scrollView: UIScrollView) {
centerScrollViewContents()
}
private var scrollViewVisibleSize: CGSize {
let contentInset = scrollView.contentInset
let scrollViewSize = scrollView.bounds.standardized.size
let width = scrollViewSize.width - contentInset.left - contentInset.right
let height = scrollViewSize.height - contentInset.top - contentInset.bottom
return CGSize(width:width, height:height)
}
private var scrollViewCenter: CGPoint {
let scrollViewSize = self.scrollViewVisibleSize()
return CGPoint(x: scrollViewSize.width / 2.0,
y: scrollViewSize.height / 2.0)
}
private func centerScrollViewContents() {
guard let image = imageView.image else {
return
}
let imgViewSize = imageView.frame.size
let imageSize = image.size
var realImgSize: CGSize
if imageSize.width / imageSize.height > imgViewSize.width / imgViewSize.height {
realImgSize = CGSize(width: imgViewSize.width,height: imgViewSize.width / imageSize.width * imageSize.height)
} else {
realImgSize = CGSize(width: imgViewSize.height / imageSize.height * imageSize.width, height: imgViewSize.height)
}
var frame = CGRect.zero
frame.size = realImgSize
imageView.frame = frame
let screenSize = scrollView.frame.size
let offx = screenSize.width > realImgSize.width ? (screenSize.width - realImgSize.width) / 2 : 0
let offy = screenSize.height > realImgSize.height ? (screenSize.height - realImgSize.height) / 2 : 0
scrollView.contentInset = UIEdgeInsets(top: offy,
left: offx,
bottom: offy,
right: offx)
// The scroll view has zoomed, so you need to re-center the contents
let scrollViewSize = scrollViewVisibleSize
// First assume that image center coincides with the contents box center.
// This is correct when the image is bigger than scrollView due to zoom
var imageCenter = CGPoint(x: scrollView.contentSize.width / 2.0,
y: scrollView.contentSize.height / 2.0)
let center = scrollViewCenter
//if image is smaller than the scrollView visible size - fix the image center accordingly
if scrollView.contentSize.width < scrollViewSize.width {
imageCenter.x = center.x
}
if scrollView.contentSize.height < scrollViewSize.height {
imageCenter.y = center.y
}
imageView.center = imageCenter
}
Why it's better than anything else I could find on SO so far:
It doesn't read or modify the UIView frame property of the image view since a zoomed image view has a transform applied to it. See here what Apple says on how to move or adjust a view size when a non identity transform is applied.
Starting iOS 7 where translucency for bars was introduced the system will auto adjust the scroll view size, scroll content insets and scroll indicators offsets. Thus you should not modify these in your code as well.
FYI:
There're check boxes for toggling this behavior (which is set by default) in the Xcode interface builder. You can find it in the view controller attributes:
The full view controller's source code is published here.
Also you can download the whole Xcode project to see the scroll view constraints setup and play around with 3 different presets in the storyboard by moving the initial controller pointer to any the following paths:
View with both translucent tab and nav bars.
View with both opaque tab and nav bars.
View with no bars at all.
Every option works correctly with the same VC implementation.
I think I got it. The solution is to use the scrollViewDidEndZooming method of the delegate and in that method set contentInset based on the size of the image. Here's what the method looks like:
- (void)scrollViewDidEndZooming:(UIScrollView *)aScrollView withView:(UIView *)view atScale:(float)scale {
CGSize imgViewSize = imageView.frame.size;
CGSize imageSize = imageView.image.size;
CGSize realImgSize;
if(imageSize.width / imageSize.height > imgViewSize.width / imgViewSize.height) {
realImgSize = CGSizeMake(imgViewSize.width, imgViewSize.width / imageSize.width * imageSize.height);
}
else {
realImgSize = CGSizeMake(imgViewSize.height / imageSize.height * imageSize.width, imgViewSize.height);
}
CGRect fr = CGRectMake(0, 0, 0, 0);
fr.size = realImgSize;
imageView.frame = fr;
CGSize scrSize = scrollView.frame.size;
float offx = (scrSize.width > realImgSize.width ? (scrSize.width - realImgSize.width) / 2 : 0);
float offy = (scrSize.height > realImgSize.height ? (scrSize.height - realImgSize.height) / 2 : 0);
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.25];
scrollView.contentInset = UIEdgeInsetsMake(offy, offx, offy, offx);
[UIView commitAnimations];
}
Note that I'm using animation on setting the inset, otherwise the image jumps inside the scrollview when the insets are added. With animation it slides to the center. I'm using UIView beginAnimation and commitAnimation instead of animation block, because I need to have the app run on iphone 3.
Here is the swift 3 version of Genk's Answer
func scrollViewDidZoom(_ scrollView: UIScrollView){
let imgViewSize:CGSize! = self.imageView.frame.size;
let imageSize:CGSize! = self.imageView.image?.size;
var realImgSize : CGSize;
if(imageSize.width / imageSize.height > imgViewSize.width / imgViewSize.height) {
realImgSize = CGSize(width: imgViewSize.width,height: imgViewSize.width / imageSize.width * imageSize.height);
}
else {
realImgSize = CGSize(width: imgViewSize.height / imageSize.height * imageSize.width, height: imgViewSize.height);
}
var fr:CGRect = CGRect.zero
fr.size = realImgSize;
self.imageView.frame = fr;
let scrSize:CGSize = scrollView.frame.size;
let offx:CGFloat = (scrSize.width > realImgSize.width ? (scrSize.width - realImgSize.width) / 2 : 0);
let offy:CGFloat = (scrSize.height > realImgSize.height ? (scrSize.height - realImgSize.height) / 2 : 0);
scrollView.contentInset = UIEdgeInsetsMake(offy, offx, offy, offx);
// The scroll view has zoomed, so you need to re-center the contents
let scrollViewSize:CGSize = self.scrollViewVisibleSize();
// First assume that image center coincides with the contents box center.
// This is correct when the image is bigger than scrollView due to zoom
var imageCenter:CGPoint = CGPoint(x: self.scrollView.contentSize.width/2.0, y:
self.scrollView.contentSize.height/2.0);
let scrollViewCenter:CGPoint = self.scrollViewCenter()
//if image is smaller than the scrollView visible size - fix the image center accordingly
if (self.scrollView.contentSize.width < scrollViewSize.width) {
imageCenter.x = scrollViewCenter.x;
}
if (self.scrollView.contentSize.height < scrollViewSize.height) {
imageCenter.y = scrollViewCenter.y;
}
self.imageView.center = imageCenter;
}
//return the scroll view center
func scrollViewCenter() -> CGPoint {
let scrollViewSize:CGSize = self.scrollViewVisibleSize()
return CGPoint(x: scrollViewSize.width/2.0, y: scrollViewSize.height/2.0);
}
// Return scrollview size without the area overlapping with tab and nav bar.
func scrollViewVisibleSize() -> CGSize{
let contentInset:UIEdgeInsets = self.scrollView.contentInset;
let scrollViewSize:CGSize = self.scrollView.bounds.standardized.size;
let width:CGFloat = scrollViewSize.width - contentInset.left - contentInset.right;
let height:CGFloat = scrollViewSize.height - contentInset.top - contentInset.bottom;
return CGSize(width:width, height:height);
}
Here is an extension tested on Swift 3.1. Just create a separate *.swift file and paste the code below:
import UIKit
extension UIScrollView {
func applyZoomToImageView() {
guard let imageView = delegate?.viewForZooming?(in: self) as? UIImageView else { return }
guard let image = imageView.image else { return }
guard imageView.frame.size.valid && image.size.valid else { return }
let size = image.size ~> imageView.frame.size
imageView.frame.size = size
self.contentInset = UIEdgeInsets(
x: self.frame.size.width ~> size.width,
y: self.frame.size.height ~> size.height
)
imageView.center = self.contentCenter
if self.contentSize.width < self.visibleSize.width {
imageView.center.x = self.visibleSize.center.x
}
if self.contentSize.height < self.visibleSize.height {
imageView.center.y = self.visibleSize.center.y
}
}
private var contentCenter: CGPoint {
return CGPoint(x: contentSize.width / 2, y: contentSize.height / 2)
}
private var visibleSize: CGSize {
let size: CGSize = bounds.standardized.size
return CGSize(
width: size.width - contentInset.left - contentInset.right,
height: size.height - contentInset.top - contentInset.bottom
)
}
}
fileprivate extension CGFloat {
static func ~>(lhs: CGFloat, rhs: CGFloat) -> CGFloat {
return lhs > rhs ? (lhs - rhs) / 2 : 0.0
}
}
fileprivate extension UIEdgeInsets {
init(x: CGFloat, y: CGFloat) {
self.bottom = y
self.left = x
self.right = x
self.top = y
}
}
fileprivate extension CGSize {
var valid: Bool {
return width > 0 && height > 0
}
var center: CGPoint {
return CGPoint(x: width / 2, y: height / 2)
}
static func ~>(lhs: CGSize, rhs: CGSize) -> CGSize {
switch lhs > rhs {
case true:
return CGSize(width: rhs.width, height: rhs.width / lhs.width * lhs.height)
default:
return CGSize(width: rhs.height / lhs.height * lhs.width, height: rhs.height)
}
}
static func >(lhs: CGSize, rhs: CGSize) -> Bool {
return lhs.width / lhs.height > rhs.width / rhs.height
}
}
The way to use:
extension YOUR_SCROLL_VIEW_DELEGATE: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return YOUR_IMAGE_VIEW
}
func scrollViewDidZoom(_ scrollView: UIScrollView){
scrollView.applyZoomToImageView()
}
}
Analogous to different answers based on setting contentInset, but shorter. Remember about setting scrollView.delegate.
func scrollViewDidZoom(_ scrollView: UIScrollView) {
let offsetX = max((scrollView.bounds.size.width - scrollView.contentSize.width) * 0.5, 0.0)
let offsetY = max((scrollView.bounds.size.height - scrollView.contentSize.height) * 0.5, 0.0)
scrollView.contentInset = UIEdgeInsets(top: offsetY, left: offsetX, bottom: offsetY, right: offsetX);
}
If you want to take a look on a few different strategies, here is a place worth to look: github & post.

Preserve Ripple effect over UIImageView

I want to preserve Ripple effect on my UIImageView. I know we can animate image for ripple effect , but preserve. In other words I want a rippled image.
I know we can animate image using
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:25.0];
[UIView setAnimationTransition:(UIViewAnimationTransition) 110 forView:imgRipple cache:NO];
[UIView commitAnimations];
But it animates , doesn't keep the ripple. I think we can get rippled image if we Pause or Stop animation before it ends. Is it possible? how can we Pause or Stop UIView Animation?
If there is any alternate to this trick ,kindly mention it.
Thanks.
I got my solution,
used this code
CFTimeInterval pausedTime = [imgRipple.layer convertTime:CACurrentMediaTime() fromLayer:nil];
imgRipple.layer.speed = 0.0;
imgRipple.layer.timeOffset = pausedTime;
and it paused
I'm not sure if it gives you what you want, but you can stop animations using those:
#import <QuartzCore/QuartzCore.h>
[CATransaction begin];
[myView.layer removeAllAnimations];
[CATransaction commit];
You can get the current state before stopping animation using presentationLayer:
CALayer* myPreLayer = [myView.layer presentationLayer];
CATransform3D currentTransform = [myPreLayer transform];
//if you need some specific info, you can use key-value pairs
float currentAngle = [[myPreLayer valueForKeyPath:#"transform.rotation.z"] floatValue];
swift 5
Imageview set ripple animation its working fine......
#IBOutlet weak var imageview: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
imageview.layer.cornerRadius = imageview.bounds.width / 2
self.animateImage()
}
func animateImage() {
addRippleEffect(to: viewAnimation)
}
func addRippleEffect(to referenceView: UIView) {
/*! Creates a circular path around the view*/
let path = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: referenceView.bounds.size.width, height: referenceView.bounds.size.height))
/*! Position where the shape layer should be */
let shapePosition = CGPoint(x: referenceView.bounds.size.width / 2.0, y: referenceView.bounds.size.height / 2.0)
let rippleShape = CAShapeLayer()
rippleShape.bounds = CGRect(x: 0, y: 0, width: referenceView.bounds.size.width, height: referenceView.bounds.size.height)
rippleShape.path = path.cgPath
rippleShape.fillColor = UIColor.clear.cgColor
rippleShape.strokeColor = UIColor.black.cgColor
rippleShape.lineWidth = 5
rippleShape.position = shapePosition
rippleShape.opacity = 0
/*! Add the ripple layer as the sublayer of the reference view */
referenceView.layer.addSublayer(rippleShape)
/*! Create scale animation of the ripples */
let scaleAnim = CABasicAnimation(keyPath: "transform.scale")
scaleAnim.fromValue = NSValue(caTransform3D: CATransform3DIdentity)
scaleAnim.toValue = NSValue(caTransform3D: CATransform3DMakeScale(2, 2, 1))
/*! Create animation for opacity of the ripples */
let opacityAnim = CABasicAnimation(keyPath: "opacity")
opacityAnim.fromValue = 1
opacityAnim.toValue = 0
/*! Group the opacity and scale animations */
let animation = CAAnimationGroup()
animation.animations = [scaleAnim, opacityAnim]
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
animation.duration = CFTimeInterval(1.0)
animation.repeatCount = .infinity
animation.isRemovedOnCompletion = true
rippleShape.add(animation, forKey: "rippleEffect")
}

adding bounce effect to appearance of UIImageView

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
})
}

UIImageView's shadow gets bigger than intended when shadowPath is set

CGRect rect = biggerImageView.bounds;
if([biggerImageView.layer respondsToSelector:#selector(setShadowColor:)])
{
float shadowOffset = rect.size.width * 0.02;
biggerImageView.layer.shadowColor = [UIColor colorWithWhite: 0.25 alpha: 0.55].CGColor;
biggerImageView.layer.shadowOffset = CGSizeMake(shadowOffset, shadowOffset);
biggerImageView.layer.shadowOpacity = 0.8;
// biggerImageView.layer.shadowPath = [UIBezierPath bezierPathWithRect: rect].CGPath;
}
The commented out line causes the shadow becomes bigger than intended.
(vertically longer shadows on top and bottom)
I looked up CALayer reference but got no clue there.
That is probably because the UIView bounds is not the real one yet. (Like in awakeFromNib or viewDidLoad)
Wait for the view layoutSubviews or viewController viewWillLayoutSubviews to be called, and update the path of your shadow there.
I created this swift extension to add the same shadow (with cornerRadius support) everywhere I need to :
import UIKit
extension UIView {
/// Call this from `layoutSubviews` or `viewWillLayoutSubviews`.
/// The shadow will be the same color as this view background.
func layoutShadow() {
// Update frame
if let shadowView = superview?.viewWithTag(978654123) {
shadowView.frame = frame
shadowView.layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
return
}
// Create the shadow the first time
let shadowView = UIView(frame: frame)
shadowView.tag = 978654123
shadowView.translatesAutoresizingMaskIntoConstraints = false
superview?.insertSubview(shadowView, belowSubview: self)
shadowView.layer.shadowColor = (backgroundColor ?? UIColor.black).cgColor
shadowView.layer.shadowOpacity = 0.8
shadowView.layer.shadowOffset = CGSize(width: -2.0, height: 4.0)
shadowView.layer.shadowRadius = 4.0
shadowView.layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
}
}

iOS icon jiggle algorithm

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.