Skip to content

Instantly share code, notes, and snippets.

@alemar11
Forked from fastred/UILabel+JumpingDots.swift
Created February 19, 2016 08:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alemar11/112e5de1250c01a7cb2b to your computer and use it in GitHub Desktop.
Save alemar11/112e5de1250c01a7cb2b to your computer and use it in GitHub Desktop.
//
// 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;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment