Created
December 12, 2017 04:10
-
-
Save rnystrom/02a8508b8840d4121e487f4d3fa37253 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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