Search code examples
swiftanimationuikituilabel

How to animate UILabel text size (and color)


With regard to animating the text size of UILabel, there seem to be a lot of "quick-fix" answers floating around Stack Overflow, posted by users who don't care much to understand what's happening under the hood.

And for design-oriented developers, this has been frustrating to see. Most answers advise specifying a transform on the UILabel, scaled by x, like this:

let label = UILabel()
label.text = "yourText"
label.sizeToFit()
label.layer.anchorPoint = CGPoint(x: 0, y: 0)
label.layer.position = CGPoint(x: 0, y: 0)
UIView.animate(withDuration: duration) {
    label.transform = CGAffineTransform(scaleX: 4.0, y: 4.0)
}

which results in the following animation:

Alt Text

But, it looks blurry — and the label's tracking values aren't adapting to the scale. So, what's the best way to animate the text size of a UILabel?


Solution

  • As I mentioned, applying a CGAffineTransform to a UILabel to scale it is frustrating for design-oriented developers, for two reasons.

    1. The transform doesn't account for tracking and font variants. San Francisco (Apple's system font) uses two distinct variants of its font depending on the text size.

    iOS automatically applies the most appropriate variant based on the point size and the user's accessibility settings. Adjust tracking—the spacing between letters—appropriately.

    SF Pro Text is applied to text 19 points or smaller, while SF Pro Display is applied to text 20 points or larger. Each variant has different "tracking" values — the spacing between letters — for each point size (see: the many tracking values under Font Usage and Tracking).

    Unfortunately, CGAffineTransform doesn't set a new pointSize, which would otherwise force a redraw of the view. Applying a transform just scales the rasterized bitmap of the UILabel, which means that fonts and tracking values aren't adaptive. So, if we're scaling our label by 2.0 from 10pt to 20pt, our resulting UILabel at 20pt will still be using SF Pro Text with 12pt tracking, instead of SF Pro Display with 19pt tracking. You can see below that the red label is transformed and hence does not adjust its font.

    1. Transforming a view or layer leads to blurriness. As previously alluded to, Core Graphics doesn't re-render the view, but rather transforms the view's 2D map of pixels. The red label below is clearly blurry.

    The Solution

    Our best bet is to use CATextLayer instead of UILabel, which Apple says is:

    A layer that provides simple text layout and rendering of plain or attributed strings.

    The CATextLayer docs list var string: Any?, var font: CFTypeRef?, var fontSize: CGFloat, var foregroundColor: CGColor? and more as properties, which most of the time is all we need. Perfect!

    All we need to do is add a CAPropertyAnimation — like CABasicAnimation — to the CATextLayer to animate its properties, like so:

    // Create the CATextLayer
    let textLayer = CATextLayer()
    textLayer.string = "yourText"
    textLayer.font = UIFont.systemFont(ofSize: startFontSize)
    textLayer.fontSize = startFontSize
    textLayer.foregroundColor = UIColor.black.cgColor
    textLayer.contentsScale = UIScreen.main.scale
    textLayer.frame = view.bounds
    view.layer.addSublayer(textLayer)
    
    // Animation
    let duration: TimeInterval = 10
    textLayer.fontSize = endFontSize
    let fontSizeAnimation = CABasicAnimation(keyPath: "fontSize")
    fontSizeAnimation.fromValue = startFontSize
    fontSizeAnimation.toValue = endFontSize
    fontSizeAnimation.duration = duration
    textLayer.add(fontSizeAnimation, forKey: nil)
    

    and voila! The black text below is our properly scaled CATextLayer. Sharp and with the correct font variant.

    Alt Text

    h/t to Yinan for the code: https://stackoverflow.com/a/42047777/4848310. Yinan also discusses how to use CATextLayer with Auto Layout constraint-based animations.


    Bonus! This also works for animating text color! Just use the foregroundColor property.