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 con
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)
}
}
Swift 3.0, 4.0
UIView.animate(withDuration: 0.5) {
label.transform = CGAffineTransform(scaleX: 1.1, y: 1.1) //Scale label area
}
Critical:
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.
I found each of the suggestions here inadequate for these reasons:
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 UILabel
s, 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
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
}