ZHocean123
10/12/2017 - 2:01 AM

A playground for TextEffectView updated for Swift 3

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