Skip to content

Instantly share code, notes, and snippets.

@rnystrom
Created December 12, 2017 04:10
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 rnystrom/02a8508b8840d4121e487f4d3fa37253 to your computer and use it in GitHub Desktop.
Save rnystrom/02a8508b8840d4121e487f4d3fa37253 to your computer and use it in GitHub Desktop.
import UIKit
extension UIContentSizeCategory {
var multiplier: CGFloat {
switch self {
case .accessibilityExtraExtraExtraLarge: return 23 / 16
case .accessibilityExtraExtraLarge: return 22 / 16
case .accessibilityExtraLarge: return 21 / 16
case .accessibilityLarge: return 20 / 16
case .accessibilityMedium: return 19 / 16
case .extraExtraExtraLarge: return 19 / 16
case .extraExtraLarge: return 18 / 16
case .extraLarge: return 17 / 16
case .large: return 1
case .medium: return 15 / 16
case .small: return 14 / 16
case .extraSmall: return 13 / 16
default: return 1
}
}
func preferredContentSize(
_ base: CGFloat,
minSize: CGFloat = 0,
maxSize: CGFloat = CGFloat.greatestFiniteMagnitude
) -> CGFloat {
let result = base * multiplier
return min(max(result, minSize), maxSize)
}
}
extension Hashable {
func combineHash<T: Hashable>(with hashableOther: T) -> Int {
let ownHash = self.hashValue
let otherHash = hashableOther.hashValue
return (ownHash << 5) &+ ownHash &+ otherHash
}
}
extension UIFont {
func addingTraits(traits: UIFontDescriptorSymbolicTraits) -> UIFont {
let newTraits = fontDescriptor.symbolicTraits.union(traits)
guard let descriptor = fontDescriptor.withSymbolicTraits(newTraits)
else { return self }
return UIFont(descriptor: descriptor, size: 0)
}
}
struct TextStyle: Hashable, Equatable {
let name: String
let size: CGFloat
let traits: UIFontDescriptorSymbolicTraits
let minSize: CGFloat
let maxSize: CGFloat
init(
name: String = UIFont.systemFont(ofSize: 1).fontName,
size: CGFloat = UIFont.systemFontSize,
traits: UIFontDescriptorSymbolicTraits = [],
minSize: CGFloat = 0,
maxSize: CGFloat = .greatestFiniteMagnitude
) {
self.name = name
self.size = size
self.traits = traits
self.minSize = minSize
self.maxSize = maxSize
self._hashValue = name
.combineHash(with: size)
.combineHash(with: traits.rawValue)
.combineHash(with: minSize)
.combineHash(with: maxSize)
}
private let _hashValue: Int
var hashValue: Int {
return _hashValue
}
static func == (lhs: TextStyle, rhs: TextStyle) -> Bool {
return lhs.name == rhs.name
&& lhs.size == rhs.size
&& lhs.traits == rhs.traits
&& lhs.minSize == rhs.minSize
&& rhs.maxSize == rhs.maxSize
}
}
struct StyledText: Hashable, Equatable {
let text: String
let style: TextStyle
let attributes: [NSAttributedStringKey: Any]
init(
text: String,
style: TextStyle = TextStyle(),
attributes: [NSAttributedStringKey: Any] = [:]
) {
self.text = text
self.style = style
self.attributes = attributes
}
func font(size: CGFloat) -> UIFont {
guard let font = UIFont(name: style.name, size: size) else {
return UIFont.systemFont(ofSize: size)
}
return font.addingTraits(traits: style.traits)
}
func render(contentSizeCategory: UIContentSizeCategory) -> NSAttributedString {
var attributes = self.attributes
attributes[.font] = font(size: contentSizeCategory.preferredContentSize(style.size))
return NSAttributedString(string: text, attributes: attributes)
}
var hashValue: Int {
return text
.combineHash(with: style)
}
static func == (lhs: StyledText, rhs: StyledText) -> Bool {
return lhs.text == rhs.text
&& lhs.style == rhs.style
}
}
struct StyledTextBuilder {
let styledTexts: [StyledText]
func adding(styledTexts: [StyledText]) -> StyledTextBuilder {
return StyledTextBuilder(styledTexts: self.styledTexts + styledTexts)
}
func adding(styledText: StyledText) -> StyledTextBuilder {
return adding(styledTexts: [styledText])
}
func adding(text: String) -> StyledTextBuilder {
guard let tip = styledTexts.last else { return self }
return adding(styledText: StyledText(text: text, style: tip.style, attributes: tip.attributes))
}
func adding(text: String, attributes: [NSAttributedStringKey: Any]) -> StyledTextBuilder {
guard let tip = styledTexts.last else { return self }
return adding(styledText: StyledText(text: text, style: tip.style, attributes: attributes))
}
func adding(
text: String,
traits: UIFontDescriptorSymbolicTraits? = nil,
attributes: [NSAttributedStringKey: Any]? = nil
) -> StyledTextBuilder {
guard let tip = styledTexts.last else { return self }
var nextAttributes = tip.attributes
if let attributes = attributes {
for (k, v) in attributes {
nextAttributes[k] = v
}
}
let nextStyle: TextStyle
if let traits = traits {
nextStyle = TextStyle(
name: tip.style.name,
size: tip.style.size,
traits: tip.style.traits.union(traits),
minSize: tip.style.minSize,
maxSize: tip.style.maxSize
)
} else {
nextStyle = tip.style
}
return adding(
styledText: StyledText(
text: text,
style: nextStyle,
attributes: nextAttributes
)
)
}
func render(contentSizeCategory: UIContentSizeCategory) -> NSAttributedString {
let result = NSMutableAttributedString()
styledTexts.forEach { result.append($0.render(contentSizeCategory: contentSizeCategory)) }
return result
}
}
let seed = StyledText(text: "foo", attributes: [.foregroundColor: UIColor.white])
let builder = StyledTextBuilder(styledTexts: [seed])
.adding(text: " bar", traits: [.traitBold, .traitItalic])
let attr = builder.render(contentSizeCategory: .medium)
import PlaygroundSupport
let someView = UILabel()
someView.attributedText = attr
someView.sizeToFit()
PlaygroundPage.current.liveView = someView
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment