Created
October 26, 2016 21:49
-
-
Save mergesort/52264be259533591557d37987ed36ae3 to your computer and use it in GitHub Desktop.
A playground for TextEffectView updated for Swift 3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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