In this episode we dive deeper into the text system, leveraging CoreText to compute exact metrics about each glyph in a string. We can use this information to do interesting things with text. There's a lot of code in this one, but you'll learn the basic foundations of text, how CoreText works, and how to extract metrics and paths from your rendered text. We'll use this in a future episode to implement some interesting effects!
Episode Links Source Code iOS Fonts - a great resource of fonts with preview, including when they became available on iOS platforms as well as their official family name for creating from code Swift: Interacting with C APIs - This is a useful reference when dealing with C APIs from Swift. There’s a lot to get tripped up on here, so keep this handy if you get stuck. Typographical Concepts - This is a must-read to understand the core concepts of all of the data you can extract about text. Core Text Overview We're creating a TextEffectView subclass that will contain all of our CoreText code. This will look like a UILabel, but we won't subclass, because that is entering a world of pain. Setting up properties public var font: UIFont! { didSet { createGlyphLayers() setNeedsDisplay() } } public var text: String?{ didSet { createGlyphLayers() setNeedsDisplay() } } public var textColor: UIColor! { didSet { createGlyphLayers() setNeedsDisplay() } } Processing the text func computeLetterPaths(attributedString: NSAttributedString) { letterPaths = [] letterPositions = [] lineRects = [] let frameSetter = CTFramesetterCreateWithAttributedString(attributedString) let textPath = CGPathCreateWithRect(bounds, nil) let textFrame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), textPath, nil) let lines = CTFrameGetLines(textFrame) var origins = [CGPoint](count: CFArrayGetCount(lines), repeatedValue: CGPointZero) CTFrameGetLineOrigins(textFrame, CFRangeMake(0, 0), &origins) for (var lineIndex = 0; lineIndex < CFArrayGetCount(lines); lineIndex++) { let unmanagedLine: UnsafePointer<Void> = CFArrayGetValueAtIndex(lines, lineIndex) let line: CTLineRef = unsafeBitCast(unmanagedLine, CTLineRef.self) var lineOrigin = origins[lineIndex] let lineBounds = CTLineGetBoundsWithOptions(line, CTLineBoundsOptions.UseGlyphPathBounds) lineRects.append(lineBounds) // adjust origin for flipped coordinate system lineOrigin.y = -CGRectGetHeight(lineBounds) let runs = CTLineGetGlyphRuns(line) for (var runIndex = 0; runIndex < CFArrayGetCount(runs); runIndex++) { let runPointer = CFArrayGetValueAtIndex(runs, runIndex) let run = unsafeBitCast(runPointer, CTRunRef.self) let attribs = CTRunGetAttributes(run) let fontPointer = CFDictionaryGetValue(attribs, unsafeAddressOf(kCTFontAttributeName)) let font = unsafeBitCast(fontPointer, CTFontRef.self) let glyphCount = CTRunGetGlyphCount(run) var ascents = [CGFloat](count: glyphCount, repeatedValue: 0) var descents = [CGFloat](count: glyphCount, repeatedValue: 0) var leading = [CGFloat](count: glyphCount, repeatedValue: 0) CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascents, &descents, &leading) for (var glyphIndex = 0; glyphIndex < glyphCount; glyphIndex++) { let glyphRange = CFRangeMake(glyphIndex, 1) var glyph = CGGlyph() var position = CGPointZero CTRunGetGlyphs(run, glyphRange, &glyph) CTRunGetPositions(run, glyphRange, &position) position.y = lineOrigin.y if let path = CTFontCreatePathForGlyph(font, glyph, nil) { letterPaths.append(UIBezierPath(CGPath: path)) letterPositions.append(position) } } } } } Creating the Layers func createGlyphLayers() { assert(NSThread.isMainThread()) guard let text = self.text else { return } if let sublayers = self.layer.sublayers { for sublayer in sublayers { sublayer.removeAllAnimations() sublayer.removeFromSuperlayer() } } let ctFont = CTFontCreateWithName(font.fontName, font.pointSize, nil) let attributedString = NSAttributedString(string: text, attributes: [ (kCTFontAttributeName as String): ctFont ]) computeLetterPaths(attributedString) let containerLayer = CALayer() containerLayer.geometryFlipped = true layer.addSublayer(containerLayer) for (index, path) in letterPaths.enumerate() { let pos = letterPositions[index] // create shape layer for this glyph let glyphLayer = CAShapeLayer() glyphLayer.path = path.CGPath glyphLayer.fillColor = textColor.CGColor glyphLayer.strokeColor = UIColor.redColor().CGColor let jitter = (CGFloat(arc4random_uniform(10)) - 5.0) / 10.0 + 1.0 glyphLayer.transform = CATransform3DMakeScale(jitter, jitter, 1.0) var glyphFrame = glyphLayer.bounds glyphFrame.origin = pos glyphLayer.frame = glyphFrame containerLayer.addSublayer(glyphLayer) } }