Skip to content

Instantly share code, notes, and snippets.

@mergesort
Created October 26, 2016 21:49
Show Gist options
  • Save mergesort/52264be259533591557d37987ed36ae3 to your computer and use it in GitHub Desktop.
Save mergesort/52264be259533591557d37987ed36ae3 to your computer and use it in GitHub Desktop.
A playground for TextEffectView updated for Swift 3
//
// TextEffectView.swift
// TextEffects
//
// Created by Ben Scheirman on 2/15/16.
// Copyright © 2016 NSScreencast. All rights reserved.
//
import UIKit
import CoreText
import PlaygroundSupport
public class TextEffectView: UIView {
// MARK: Public properties
public var font: UIFont = UIFont.systemFont(ofSize: 14) {
didSet {
self.createGlyphLayers()
setNeedsDisplay()
}
}
public var text: String? {
didSet {
self.createGlyphLayers()
setNeedsDisplay()
}
}
public var textColor: UIColor = UIColor.black {
didSet {
self.createGlyphLayers()
setNeedsDisplay()
}
}
var letterPaths: [UIBezierPath] = []
// MARK: Private properties
private var lineRects: [CGRect] = []
private var letterPositions: [CGPoint] = []
public override init(frame: CGRect) {
super.init(frame: frame)
}
public required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Drawing
func createGlyphLayers() {
guard let text = self.text else { return }
self.layer.sublayers?.forEach({
$0.removeAllAnimations()
$0.removeFromSuperlayer()
})
let ctFont = CTFontCreateWithName(self.font.fontName as CFString?, font.pointSize, nil)
let attributedString = NSAttributedString(string: text, attributes: [ (kCTFontAttributeName as String): ctFont ])
self.computeLetterPaths(attributedString: attributedString)
let containerLayer = CALayer()
containerLayer.isGeometryFlipped = true
layer.addSublayer(containerLayer)
for (index, path) in letterPaths.enumerated() {
let pos = letterPositions[index]
let glyphLayer = CAShapeLayer()
glyphLayer.path = path.cgPath
glyphLayer.fillColor = textColor.cgColor
self.processGlyphLayer(layer: glyphLayer, atIndex: index)
var glyphFrame = glyphLayer.bounds
glyphFrame.origin = pos
glyphLayer.frame = glyphFrame
containerLayer.addSublayer(glyphLayer)
print(glyphFrame)
}
print(self.lineRects)
print(self.letterPositions)
}
func processGlyphLayer(layer: CAShapeLayer, atIndex index: Int) {
fatalError("Must implement processGlyphLayer(layer: atIndex:")
}
private func computeLetterPaths(attributedString: NSAttributedString) {
self.letterPaths = []
self.letterPositions = []
self.lineRects = []
let frameSetter = CTFramesetterCreateWithAttributedString(attributedString)
let textPath = CGPath(rect: bounds, transform: nil)
let textFrame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), textPath, nil)
let lines = CTFrameGetLines(textFrame)
var origins = [CGPoint](repeating: .zero, count: CFArrayGetCount(lines))
CTFrameGetLineOrigins(textFrame, CFRangeMake(0, 0), &origins)
for lineIndex in 0 ..< CFArrayGetCount(lines) {
let unmanagedLine: UnsafeRawPointer = CFArrayGetValueAtIndex(lines, lineIndex)
let line: CTLine = unsafeBitCast(unmanagedLine, to: CTLine.self)
var lineOrigin = origins[lineIndex]
let lineBounds = CTLineGetBoundsWithOptions(line, CTLineBoundsOptions.useGlyphPathBounds)
lineRects.append(lineBounds)
// adjust origin for flipped coordinate system
lineOrigin.y = -lineBounds.height
let runs = CTLineGetGlyphRuns(line)
for runIndex in 0 ..< CFArrayGetCount(runs) {
let runPointer = CFArrayGetValueAtIndex(runs, runIndex)
let run = unsafeBitCast(runPointer, to: CTRun.self)
let attribs = CTRunGetAttributes(run)
let fontPointer = CFDictionaryGetValue(attribs, Unmanaged.passUnretained(kCTFontAttributeName).toOpaque())
let font = unsafeBitCast(fontPointer, to: CTFont.self)
let glyphCount = CTRunGetGlyphCount(run)
var ascents = [CGFloat](repeating: 0, count: glyphCount)
var descents = [CGFloat](repeating: 0, count: glyphCount)
var leading = [CGFloat](repeating: 0, count: glyphCount)
CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascents, &descents, &leading)
for glyphIndex in 0 ..< glyphCount {
let glyphRange = CFRangeMake(glyphIndex, 1)
var glyph = CGGlyph()
var position = CGPoint.zero
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)
}
}
}
}
}
public override var intrinsicContentSize: CGSize {
return self.lineRects.first?.size ?? .zero
}
}
class TypingTextEffectView : TextEffectView {
var letterDuration: TimeInterval = 0.5
var letterDelay: TimeInterval = 0.03
override func processGlyphLayer(layer: CAShapeLayer, atIndex index: Int) {
layer.opacity = 0
layer.fillColor = UIColor.darkGray.cgColor
layer.lineWidth = 0
let opacityAnim = CABasicAnimation(keyPath: "opacity")
opacityAnim.fromValue = 0
opacityAnim.toValue = 1
opacityAnim.duration = letterDuration
let rotateAnim = CABasicAnimation(keyPath: "transform.rotation")
rotateAnim.fromValue = -M_PI / 4.0
rotateAnim.toValue = 0
rotateAnim.duration = letterDuration / 2.0
let scaleAnim = CAKeyframeAnimation(keyPath: "transform.scale")
scaleAnim.values = [1.4, 0.9, 1.0]
scaleAnim.keyTimes = [0, 0.75, 1.0]
scaleAnim.duration = letterDuration
let group = CAAnimationGroup()
group.animations = [opacityAnim, rotateAnim, scaleAnim]
group.duration = letterDuration
group.beginTime = CACurrentMediaTime() + Double(index) * letterDelay
group.fillMode = kCAFillModeForwards
group.isRemovedOnCompletion = false
layer.add(group, forKey: "animationGroup")
}
}
let textEffectView = TypingTextEffectView(frame: CGRect(x: 0.0, y: 0.0, width: 416, height: 200.0))
textEffectView.backgroundColor = UIColor.white
textEffectView.font = UIFont(name: "AvenirNext-Regular", size: 40)!
textEffectView.text = "Hello, Core Text"
PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.liveView = textEffectView
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment