Animating text layers using CoreAnimation and CoreText frameworks in iOS SDK

CoreAnimation is a very powerful framework. By using CABasicAnimation, CAKeyFrameAnimation or CAAnimationGroup we can create amazing user experience and there are basically no limits. It is up to our imagination what exactly will be presented on a screen and how it will behave. It turns out that CoreAnimation can be also used with other frameworks such as CoreGraphics or CoreText, which gives a developer the ability to create amazing animations. Using CoreText with CoreAnimation we can animate every text with a chosen font type and font size. In this article I am going to present a way how to do just that.

 

CoreText basics

Firstly, let me present to you the CoreText basics. In order to animate text layers, we have to know what a line or a glyph is. This will help us understand the whole process of generating a path from a text. Apple has laid out for us the basics of CoreText. All of it canbe found here. Below I have gathered the most crucial information.

 

Fig 1.1 iOS CoreText basics

Figure 1 – Core Text architecture, source: http://developer.apple.com

 

At the top of this hierarchy is the frame setter object. With an attributed string and a graphics path as input. A frame setter generates one or more frames of text. Each frame object represents a paragraph. To generate frames, the frame setter calls a typesetter object. When a frame setter lays a particular frame, it applies a paragraph style to it including such attributes as alignment, tab stops or line spacing. The typesetter converts the characters in the attributed string to glyphs and fits those glyphs into the lines that fill a text frame.
Each frame object contains the paragraph’s line objects, which represent a single line of text within a paragraph. A frame object may contain just a single long line object or it might contain a set of lines. Line objects are created by the typesetter during a frame setting operation.

Each line object contains an array of glyph run objects. A glyph run is a set of consecutive glyphs that share the same attributes and direction. The typesetter creates glyph runs as it produces lines from character strings, attributes, and font objects. This means that a line is constructed of one or more glyphs runs.  At the end of the hierarchy is a glyph object which usually represents a single character of text.

After we have familiarized ourselves with the CoreText basics we can now proceed with text layer animations.

 

Animating text layer

In order to animate a text layer, we need to convert it to a CGPath object. First we have to create an attributed string which we want to animate, then we have to create a line object from this string in order to get glyph runs. The code below shows how we can achieve these objects:

 

let attrString  = NSAttributedString(string: text, attributes: [kCTFontAttributeName as String : font])
let line        = CTLineCreateWithAttributedString(attrString)
let runArray    = CTLineGetGlyphRuns(line)

            When we have all our glyph runs stored in a runArray. We can iterate through this array and for each glyph run we can get the font and all corresponding glyphs. Now we can iterate through all glyphs in order to single out a specific glyph object. When we have this object we can calculate its position and also transform it to path object using CTFontCreatePathForGlyph function. By having a path and knowing its position we are able to build an overall path which is constructed from glyph paths. The below pseudo code shows the whole algorithm:

 

%For each glyph run
for runIndex in 0..<CFArrayGetCount(runArray) {

     let runFont = %Get a font from a single glyph

     %For each glyph in a single glyph run
     for runGlyphIndex in 0..<CTRunGetGlyphCount(run) {
                var glyph    = %Get a glyph
                var position = %Get glyph’s position

                %Get a letter path from a single glyph
                let letter = CTFontCreatePathForGlyph(runFont, glyph, nil)
                 %Make a translation to a desired position
                var t = CGAffineTransformMakeTranslation(position.x, position.y)
                 %Add a single letter path to the whole path  
                CGPathAddPath(letters, &t, letter)
            }
     }

 

After we have transformed our text into a path object and added it to the pathLayer we can simply animate it using this code:


let pathAnimation       = CABasicAnimation(keyPath: "strokeEnd")
pathAnimation.duration  = duration
pathAnimation.fromValue = 0.0
pathAnimation.toValue   = 1.0
pathAnimation.delegate  = self
pathLayer?.addAnimation(pathAnimation, forKey: "strokeEnd")

For the purpose of text layer animation, a VRMTextAnimator class was created. The class and example project demonstrating text layer animations can be found here.

            In order to use the class it has to be initialized with a reference view using the init(referenceView:) method. Reference view is a type of view where we can perform text animations. VRMTextAnimator has two key properties: animationLayer and pathLayer. The first one is a CALayer object on which we are going to perform animations, the second one is a CAShapeLayer object which stores a CGPath object created from the text we have set before. The class has 3 settable properties: fontName, fontSize, textToAnimate. If we want to animate a particular text with it’s font type and font size, we need to set those properties. VRMTextAnimator has also a delegate object which can inform us about beginning and end of the animation.

 

textAnimator_colored

 

In the project, there are two types of text layer animations. First animation is simply triggered by a start animation button. The second animation is controlled by a slider. It turns out that we can control animation by setting its speed, fromValue, toValue, duration and timeOffset properties. If we set fromValue to 0, toValue to 1, duration to 1 and we are animating strokeEnd property of our pathLayer object, we are able to draw a corresponding part of the text’s outline just by setting timeOffset property of our pathLayer.