duguyihou
2/14/2016 - 6:59 PM

JumpingDots http://holko.pl/2016/02/16/enhancing-uiviews/

//
//  UILabel+JumpingDots.swift
//  JumpingDots
//
//  Copyright (c) 2016 Arkadiusz Holko. All rights reserved.
//

import UIKit
import ObjectiveC

enum JumpingDotsError: ErrorType {
    case MissingAttributedString
    case DoesNotEndWithThreeDots
}

private class LabelTextStackReplica {

    let attributedString: NSAttributedString
    let textStorage: NSTextStorage
    let layoutManager: NSLayoutManager
    let textContainer: NSTextContainer

    init(attributedString: NSAttributedString, size: CGSize) {
        self.attributedString = attributedString

        layoutManager = NSLayoutManager()
        textStorage = NSTextStorage(attributedString: attributedString)
        textStorage.addLayoutManager(layoutManager)

        textContainer = NSTextContainer(size: size)
        textContainer.lineFragmentPadding = 0
        layoutManager.addTextContainer(textContainer)
    }
}

extension UILabel {
    
    private struct AssociatedKeys {
        static var JumpingDotsRunning = "ahk_jumpingDotsRunning"
        static var JumpingDotsPausing = "ahk_jumpingDotsPausing"
    }
    
    private(set) var jumpingDotsRunning: Bool {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.JumpingDotsRunning) as? Bool ?? false
        }
        set {
            objc_setAssociatedObject(self, &AssociatedKeys.JumpingDotsRunning, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
    
    private var jumpingDotsPausing: Bool {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.JumpingDotsPausing) as? Bool ?? false
        }
        set {
            objc_setAssociatedObject(self, &AssociatedKeys.JumpingDotsPausing, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
    
    func startJumpingDots() throws {
        let requiredEnding = "..."

        guard let attributedText = attributedText else {
            throw JumpingDotsError.MissingAttributedString
        }
        
        let text = attributedText.string

        guard text.hasSuffix(requiredEnding) else {
            throw JumpingDotsError.DoesNotEndWithThreeDots
        }

        let endingCharacterCount = requiredEnding.characters.count
        let endingRange = NSRange(location: text.characters.count - endingCharacterCount, length: endingCharacterCount)

        jumpingDotsRunning = true

        var addedSubviews: [UIView] = []
        
        let originalTextColor = attributedText.attribute(NSForegroundColorAttributeName, atIndex: endingRange.location, effectiveRange: nil) as? UIColor
        
        for i in 0..<endingCharacterCount {
            let characterPosition = text.characters.count - endingCharacterCount + i
            let boundingRect = boundingRectForCharacterAtPosition(characterPosition, inAttributedString: attributedText).integral

            let imageView = UIImageView(frame: boundingRect)
            imageView.image = snapshotRect(boundingRect)
            addSubview(imageView)
            addedSubviews.append(imageView)

            if i == endingCharacterCount - 1 {
                changeTextColorAtRange(endingRange, to: UIColor.clearColor())
            }
            
            let delay = Double(i) * 0.15
            UIView.animateKeyframesWithDuration(1.15, delay: delay, options: [], animations: {
                let relativeDuration = 0.282
                
                UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: relativeDuration, animations: {
                    imageView.frame.origin.y = -self.bounds.height / 4
                })
                
                UIView.addKeyframeWithRelativeStartTime(relativeDuration, relativeDuration: relativeDuration, animations: {
                    imageView.frame.origin.y = 0
                })
                }, completion: { [weak self] finished in
                    if i == endingCharacterCount - 1 {
                        self?.triggerNextIterationIfNeededUsingEndingRange(endingRange, addedSubviews: addedSubviews, originalTextColor: originalTextColor)
                    }
                }
            )
        }
    }

    private func triggerNextIterationIfNeededUsingEndingRange(endingRange: NSRange, addedSubviews: [UIView], originalTextColor: UIColor?) {
        if let color = originalTextColor {
            changeTextColorAtRange(endingRange, to: color)
        }

        for subview in addedSubviews {
            subview.removeFromSuperview()
        }

        let resetState = {
            self.jumpingDotsPausing = false
            self.jumpingDotsRunning = false
        }

        if self.jumpingDotsPausing {
            resetState()
        }

        if self.jumpingDotsRunning {
            do {
                try startJumpingDots()
            } catch {
                resetState()
            }
        }
    }

    func stopJumpingDots() {
        jumpingDotsPausing = true
    }

    private func changeTextColorAtRange(range: NSRange, to color: UIColor) {
        if let mutableAttributedString = attributedText?.mutableCopy() {
            mutableAttributedString.addAttribute(NSForegroundColorAttributeName, value: color, range: range)
            self.attributedText = mutableAttributedString.copy() as? NSAttributedString
        }
    }

    private func boundingRectForCharacterAtPosition(characterPosition: Int, inAttributedString attributedString: NSAttributedString) -> CGRect {
        let textStack = LabelTextStackReplica(attributedString: attributedString, size: frame.size)

        let range = NSRange(location: characterPosition, length: 1)
        var glyphRange = NSRange()
        textStack.layoutManager.characterRangeForGlyphRange(range, actualGlyphRange: &glyphRange)
        
        return textStack.layoutManager.boundingRectForGlyphRange(glyphRange, inTextContainer: textStack.textContainer)
    }
}

extension UIView {
    public func snapshotRect(rect: CGRect) -> UIImage {
        UIGraphicsBeginImageContextWithOptions(rect.size, false, UIScreen.mainScreen().scale)

        let offsetRect = CGRectOffset(bounds, -rect.origin.x, -rect.origin.y)
        let success = drawViewHierarchyInRect(offsetRect, afterScreenUpdates: false)
        assert(success)

        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        return image;
    }
}