Files
GitHawk/Pods/StyledTextKit/Source/StyledTextRenderer.swift
2018-07-15 15:25:47 -04:00

168 lines
5.6 KiB
Swift

//
// StyledTextKitRenderer.swift
// StyledTextKit
//
// Created by Ryan Nystrom on 12/13/17.
// Copyright © 2017 Ryan Nystrom. All rights reserved.
//
import UIKit
public final class StyledTextRenderer {
internal let layoutManager: NSLayoutManager
internal let textContainer: NSTextContainer
public let scale: CGFloat
public let inset: UIEdgeInsets
public let string: StyledTextString
public let backgroundColor: UIColor?
private var map = [UIContentSizeCategory: NSTextStorage]()
private var lock = os_unfair_lock_s()
private var contentSizeCategory: UIContentSizeCategory
public init(
string: StyledTextString,
contentSizeCategory: UIContentSizeCategory,
inset: UIEdgeInsets = .zero,
backgroundColor: UIColor? = nil,
layoutManager: NSLayoutManager = NSLayoutManager(),
scale: CGFloat = StyledTextScreenScale,
maximumNumberOfLines: Int = 0
) {
self.string = string
self.contentSizeCategory = contentSizeCategory
self.inset = inset
self.backgroundColor = backgroundColor
self.scale = scale
textContainer = NSTextContainer()
textContainer.exclusionPaths = []
textContainer.maximumNumberOfLines = maximumNumberOfLines
textContainer.lineFragmentPadding = 0
self.layoutManager = layoutManager
layoutManager.allowsNonContiguousLayout = false
layoutManager.hyphenationFactor = 0
layoutManager.showsInvisibleCharacters = false
layoutManager.showsControlCharacters = false
layoutManager.usesFontLeading = true
layoutManager.addTextContainer(textContainer)
}
internal var storage: NSTextStorage {
if let storage = map[contentSizeCategory] {
return storage
}
let storage = NSTextStorage(attributedString: string.render(contentSizeCategory: contentSizeCategory))
storage.addLayoutManager(layoutManager)
map[contentSizeCategory] = storage
return storage
}
private static let globalSizeCache = LRUCache<StyledTextRenderCacheKey, CGSize>(
maxSize: 1000, // CGSize cache size always 1, treat as item count
compaction: .default,
clearOnWarning: true
)
// not thread safe
private func _size(_ key: StyledTextRenderCacheKey) -> CGSize {
let cache = StyledTextRenderer.globalSizeCache
if let cached = cache[key] {
// always update the container to requested size
textContainer.size = cached
return cached
}
let insetWidth = max(key.width - inset.left - inset.right, 0)
let size = layoutManager.size(textContainer: textContainer, width: insetWidth, scale: scale)
cache[key] = size
return size
}
public func size(in width: CGFloat = .greatestFiniteMagnitude) -> CGSize {
os_unfair_lock_lock(&lock)
defer { os_unfair_lock_unlock(&lock) }
return _size(StyledTextRenderCacheKey(width: width, attributedText: storage, backgroundColor: backgroundColor, maximumNumberOfLines: textContainer.maximumNumberOfLines))
}
public func viewSize(in width: CGFloat = .greatestFiniteMagnitude) -> CGSize {
return size(in: width).resized(inset: inset)
}
private static let globalBitmapCache = LRUCache<StyledTextRenderCacheKey, CGImage>(
maxSize: 1024 * 1024 * 20, // 20mb
compaction: .default,
clearOnWarning: true
)
public func cachedRender(for width: CGFloat) -> (image: CGImage?, size: CGSize?) {
os_unfair_lock_lock(&lock)
defer { os_unfair_lock_unlock(&lock) }
let key = StyledTextRenderCacheKey(
width: width,
attributedText: storage,
backgroundColor: backgroundColor,
maximumNumberOfLines: 0
)
return (StyledTextRenderer.globalBitmapCache[key], StyledTextRenderer.globalSizeCache[key])
}
public func render(for width: CGFloat) -> (image: CGImage?, size: CGSize) {
os_unfair_lock_lock(&lock)
defer { os_unfair_lock_unlock(&lock) }
let key = StyledTextRenderCacheKey(width: width, attributedText: storage, backgroundColor: backgroundColor, maximumNumberOfLines: textContainer.maximumNumberOfLines)
let size = _size(key)
let cache = StyledTextRenderer.globalBitmapCache
if let cached = cache[key] {
return (cached, size)
}
let contents = layoutManager.render(
size: size,
textContainer: textContainer,
scale: scale,
backgroundColor: backgroundColor
)
cache[key] = contents
return (contents, size)
}
public func attributes(at point: CGPoint) -> (attributes: [NSAttributedStringKey: Any], index: Int)? {
os_unfair_lock_lock(&lock)
defer { os_unfair_lock_unlock(&lock) }
var fractionDistance: CGFloat = 1.0
let index = layoutManager.characterIndex(
for: point,
in: textContainer,
fractionOfDistanceBetweenInsertionPoints: &fractionDistance
)
if index != NSNotFound,
fractionDistance < 1.0,
let attributes = layoutManager.textStorage?.attributes(at: index, effectiveRange: nil) {
return (attributes, index)
}
return nil
}
public enum WarmOption {
case size
case bitmap
}
public func warm(
_ option: WarmOption = .size,
width: CGFloat
) -> StyledTextRenderer {
switch option {
case .size: let _ = size(in: width)
case .bitmap: let _ = render(for: width)
}
return self
}
}