Last active
February 9, 2022 06:09
-
-
Save raygun101/ceb8733d6472cab06265836d4ca28d73 to your computer and use it in GitHub Desktop.
π° Layered Cakewalk [Swift] - AttributesStyle
This file contains hidden or 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 | |
import PlaygroundSupport | |
/// | |
/// π° Layered Cakewalk - AttributesStyle | |
/// | |
/// Allows you to structure style classes used to modify an `NSAttributedString`s. | |
/// | |
// | |
// π Demo | |
// | |
func exampleLines() -> [NSAttributedString] | |
{ | |
let base = NSAttributedString.AttributesStyle().defaults() /// π¦ base/defaults style is required to process the `fragments`. | |
let style1 = NSAttributedString.AttributesStyle(foregroundColor: .red) | |
let style2 = NSAttributedString.AttributesStyle(fontFragment: .init(size: 40), foregroundColor: .blue) | |
let style3 = NSAttributedString.AttributesStyle(underlineStyle: .double) | |
return | |
[ | |
"Hello World!" | |
+ base | |
+ style1 /// `full range` | |
+ (style: style2, range: NSRange(location: 0, length: 4)) | |
+ (style: style3, range: NSRange(location: 3, length: 4)) | |
] | |
} | |
// | |
// π° Implementation | |
// | |
extension NSAttributedString | |
{ | |
public typealias Attributes = [Key: Any] | |
} | |
/// π`Meta tags` | |
/// | |
extension NSAttributedString.Key | |
{ | |
public static let fontFragment = NSAttributedString.Key("@fontFragment") | |
public static let paragraphStyleFragment = NSAttributedString.Key("@paragraphStyleFragment") | |
} | |
extension NSAttributedString.Attributes | |
{ | |
public func with(_ attributesStyle: NSAttributedString.AttributesStyle) -> NSAttributedString.Attributes | |
{ | |
var result = self | |
/// π `font` or `fontFragment` | |
/// π | |
if let fontFragment = attributesStyle.fontFragment | |
{ | |
let currentFontFragment = result[.fontFragment] as! NSAttributedString.FontFragment? | |
let newFontFragment = currentFontFragment?.with(fontFragment) ?? fontFragment | |
if let currentFont = result[.font] as! UIFont? ?? attributesStyle.defaultFont | |
{ | |
result[.font] = newFontFragment.font(basedOn: currentFont.fontDescriptor) | |
result[.fontFragment] = nil | |
} | |
else | |
{ | |
result[.fontFragment] = newFontFragment | |
} | |
} | |
else | |
if let defaultFont = attributesStyle.defaultFont | |
{ | |
if let currentFontFragment = result[.fontFragment] as! NSAttributedString.FontFragment? | |
{ | |
result[.font] = currentFontFragment.font(basedOn: defaultFont.fontDescriptor) | |
result[.fontFragment] = nil | |
} | |
else | |
if result[.font] == nil | |
{ | |
result[.font] = defaultFont | |
} | |
} | |
/// π `paragraphStyle` or `paragraphStyleFragment` | |
/// π | |
if let paragraphStyleFragment = attributesStyle.paragraphStyleFragment | |
{ | |
let currentParagraphStyleFragment = result[.paragraphStyleFragment] as! NSAttributedString.ParagraphStyleFragment? | |
let newParagraphStyleFragment = currentParagraphStyleFragment?.with(paragraphStyleFragment) ?? paragraphStyleFragment | |
if let currentParagraphStyle = result[.paragraphStyle] as! NSParagraphStyle? ?? attributesStyle.defaultParagraphStyle | |
{ | |
result[.paragraphStyle] = newParagraphStyleFragment.paragraphStyle(basedOn: currentParagraphStyle) | |
result[.paragraphStyleFragment] = nil | |
} | |
else | |
{ | |
result[.paragraphStyleFragment] = newParagraphStyleFragment | |
} | |
} | |
else | |
if let defaultParagraphStyle = attributesStyle.defaultParagraphStyle | |
{ | |
if let currentParagraphStyleFragment = result[.paragraphStyleFragment] as! NSAttributedString.ParagraphStyleFragment? | |
{ | |
result[.paragraphStyle] = currentParagraphStyleFragment.paragraphStyle(basedOn: defaultParagraphStyle) | |
result[.paragraphStyleFragment] = nil | |
} | |
else | |
if result[.paragraphStyle] == nil | |
{ | |
result[.paragraphStyle] = defaultParagraphStyle | |
} | |
} | |
/// π `foregroundColor` | |
if let foregroundColor = attributesStyle.foregroundColor | |
{ | |
result[.foregroundColor] = foregroundColor | |
} | |
else | |
if let defaultForegroundColor = attributesStyle.defaultForegroundColor, result[.foregroundColor] == nil | |
{ | |
result[.foregroundColor] = defaultForegroundColor | |
} | |
/// π `underlineStyle` | |
if let underlineStyle = attributesStyle.underlineStyle | |
{ | |
result[.underlineStyle] = underlineStyle.rawValue | |
} | |
else | |
if let defaultUnderlineStyle = attributesStyle.defaultUnderlineStyle, result[.underlineStyle] == nil | |
{ | |
result[.underlineStyle] = defaultUnderlineStyle.rawValue | |
} | |
/// π `shadow` | |
if let shadow = attributesStyle.shadow | |
{ | |
result[.shadow] = shadow | |
} | |
return result | |
} | |
} | |
extension NSAttributedString | |
{ | |
public struct FontFragment | |
{ | |
static let systemFontPrefix = ".SFUI-" | |
public var font: UIFont? | |
public var name: String? | |
public var family: String? | |
public var size: CGFloat? | |
public var weight: UIFont.Weight? | |
public var symbolicTraits: UIFontDescriptor.SymbolicTraits? | |
public var addSymbolicTraits: UIFontDescriptor.SymbolicTraits? | |
public var removeSymbolicTraits: UIFontDescriptor.SymbolicTraits? | |
public init(font: UIFont? = nil, name: String? = nil, family: String? = nil, size: CGFloat? = nil, weight: UIFont.Weight? = nil, removeWeight: Bool? = nil, | |
symbolicTraits: UIFontDescriptor.SymbolicTraits? = nil, addSymbolicTraits: UIFontDescriptor.SymbolicTraits? = nil, removeSymbolicTraits: UIFontDescriptor.SymbolicTraits? = nil) | |
{ | |
self.font = font | |
self.name = name | |
self.family = family | |
self.size = size | |
self.weight = weight | |
self.symbolicTraits = symbolicTraits | |
self.addSymbolicTraits = addSymbolicTraits | |
self.removeSymbolicTraits = removeSymbolicTraits | |
} | |
public func with(_ other: FontFragment) -> FontFragment | |
{ | |
return FontFragment(font: other.font ?? self.font, | |
name: other.name ?? self.name, | |
family: other.family ?? self.family, | |
size: other.size ?? self.size, | |
weight: other.weight ?? self.weight, | |
symbolicTraits: other.symbolicTraits ?? self.symbolicTraits, | |
addSymbolicTraits: other.addSymbolicTraits ?? self.addSymbolicTraits, | |
removeSymbolicTraits: other.removeSymbolicTraits ?? self.removeSymbolicTraits) | |
} | |
public func fontDescriptor(basedOn: UIFontDescriptor) -> UIFontDescriptor | |
{ | |
var result = self.font?.fontDescriptor ?? basedOn | |
if let name = self.name, name != basedOn.postscriptName | |
{ | |
/// Compiler warning: | |
/// CoreText note: Client requested name `".SFUI-Regular"`, it will get `TimesNewRomanPSMT` rather than the intended font. | |
/// All system UI font access should be through proper APIs such as `CTFontCreateUIFontForLanguage()` or `+[UIFont systemFontOfSize:]`. | |
/// | |
if name.hasPrefix(Self.systemFontPrefix) | |
{ | |
result = UIFont.systemFont(ofSize: basedOn.pointSize).fontDescriptor | |
} | |
else | |
{ | |
result = UIFont(name: name, size: basedOn.pointSize)!.fontDescriptor | |
} | |
result = result.withSymbolicTraits(basedOn.symbolicTraits) ?? result | |
} | |
if let weight = self.weight | |
{ | |
result = result.addingAttributes([.traits : [UIFontDescriptor.TraitKey.weight : weight]]) | |
} | |
if let family = self.family | |
{ | |
result = result.withFamily(family) | |
} | |
if let size = self.size | |
{ | |
result = result.withSize(size) | |
} | |
if self.symbolicTraits != nil || self.addSymbolicTraits != nil || self.removeSymbolicTraits != nil | |
{ | |
var newTraits = self.symbolicTraits ?? result.symbolicTraits | |
if let addSymbolicTraits = self.addSymbolicTraits | |
{ | |
newTraits.insert(addSymbolicTraits) | |
} | |
if let removeSymbolicTraits = self.removeSymbolicTraits | |
{ | |
newTraits.remove(removeSymbolicTraits) | |
} | |
result = result.withSymbolicTraits(newTraits) ?? result | |
} | |
return result | |
} | |
public func font(basedOn: UIFontDescriptor) -> UIFont | |
{ | |
let newFontDescriptor = self.fontDescriptor(basedOn: basedOn) | |
return UIFont(descriptor: newFontDescriptor, size: newFontDescriptor.pointSize) | |
} | |
} | |
} | |
extension NSAttributedString | |
{ | |
public struct ParagraphStyleFragment | |
{ | |
public var alignment: NSTextAlignment? | |
public var lineBreakMode: NSLineBreakMode? | |
public var lineSpacing: CGFloat? | |
public var lineHeightMultiple: CGFloat? | |
public var paragraphSpacingBefore: CGFloat? | |
public var headIndent: CGFloat? | |
public var tailIndent: CGFloat? | |
public init(alignment: NSTextAlignment? = nil, lineBreakMode: NSLineBreakMode? = nil, lineSpacing: CGFloat? = nil, lineHeightMultiple: CGFloat? = nil, | |
// | |
paragraphSpacingBefore: CGFloat? = nil, headIndent: CGFloat? = nil, tailIndent: CGFloat? = nil) | |
{ | |
self.alignment = alignment | |
self.lineBreakMode = lineBreakMode | |
self.lineSpacing = lineSpacing | |
self.lineHeightMultiple = lineHeightMultiple | |
self.paragraphSpacingBefore = paragraphSpacingBefore | |
self.headIndent = headIndent | |
self.tailIndent = tailIndent | |
} | |
public func with(_ other: ParagraphStyleFragment) -> ParagraphStyleFragment | |
{ | |
return ParagraphStyleFragment(alignment: other.alignment ?? self.alignment, | |
lineBreakMode: other.lineBreakMode ?? self.lineBreakMode, | |
lineSpacing: other.lineSpacing ?? self.lineSpacing, | |
lineHeightMultiple: other.lineHeightMultiple ?? self.lineHeightMultiple, | |
paragraphSpacingBefore: other.paragraphSpacingBefore ?? self.paragraphSpacingBefore, | |
headIndent: other.headIndent ?? self.headIndent, | |
tailIndent: other.tailIndent ?? self.tailIndent) | |
} | |
public func paragraphStyle(basedOn: NSParagraphStyle) -> NSParagraphStyle | |
{ | |
let result = NSMutableParagraphStyle() | |
// | |
result.alignment = self.alignment ?? basedOn.alignment | |
result.lineBreakMode = self.lineBreakMode ?? basedOn.lineBreakMode | |
result.lineSpacing = self.lineSpacing ?? basedOn.lineSpacing | |
result.lineHeightMultiple = self.lineHeightMultiple ?? basedOn.lineHeightMultiple | |
result.paragraphSpacingBefore = self.paragraphSpacingBefore ?? basedOn.paragraphSpacingBefore | |
result.headIndent = self.headIndent ?? basedOn.headIndent | |
result.tailIndent = self.tailIndent ?? basedOn.tailIndent | |
return result | |
} | |
} | |
} | |
extension NSAttributedString | |
{ | |
public struct AttributesStyle | |
{ | |
public var defaultFont: UIFont? | |
public var fontFragment: FontFragment? | |
// | |
public var defaultParagraphStyle: NSParagraphStyle? | |
public var paragraphStyleFragment: ParagraphStyleFragment? | |
// | |
public var defaultForegroundColor: UIColor? | |
public var foregroundColor: UIColor? | |
public var defaultUnderlineStyle: NSUnderlineStyle? | |
public var underlineStyle: NSUnderlineStyle? | |
// | |
public var shadow: NSShadow? | |
public init(defaultFont: UIFont? = nil, fontFragment: NSAttributedString.FontFragment? = nil, | |
// | |
defaultParagraphStyle: NSParagraphStyle? = nil, paragraphStyleFragment: NSAttributedString.ParagraphStyleFragment? = nil, | |
// | |
defaultForegroundColor: UIColor? = nil, foregroundColor: UIColor? = nil, | |
defaultUnderlineStyle: NSUnderlineStyle? = nil, underlineStyle: NSUnderlineStyle? = nil, | |
// | |
shadow: NSShadow? = nil) | |
{ | |
self.defaultFont = defaultFont | |
self.fontFragment = fontFragment | |
// | |
self.defaultParagraphStyle = defaultParagraphStyle | |
self.paragraphStyleFragment = paragraphStyleFragment | |
// | |
self.defaultForegroundColor = defaultForegroundColor | |
self.foregroundColor = foregroundColor | |
self.defaultUnderlineStyle = defaultUnderlineStyle | |
self.underlineStyle = underlineStyle | |
// | |
self.shadow = shadow | |
} | |
public func with(_ other: AttributesStyle) -> AttributesStyle | |
{ | |
return AttributesStyle(defaultFont: other.defaultFont ?? self.defaultFont, | |
fontFragment: other.fontFragment .map { self.fontFragment?.with($0) ?? $0 } ?? self.fontFragment, | |
// | |
defaultParagraphStyle: other.defaultParagraphStyle ?? self.defaultParagraphStyle, | |
paragraphStyleFragment: other.paragraphStyleFragment .map { self.paragraphStyleFragment?.with($0) ?? $0 } ?? self.paragraphStyleFragment, | |
// | |
defaultForegroundColor: other.defaultForegroundColor ?? self.defaultForegroundColor, | |
foregroundColor: other.foregroundColor ?? self.foregroundColor, | |
defaultUnderlineStyle: other.defaultUnderlineStyle ?? self.defaultUnderlineStyle, | |
underlineStyle: other.underlineStyle ?? self.underlineStyle, | |
// | |
shadow: other.shadow ?? self.shadow) | |
} | |
public func defaults() -> AttributesStyle | |
{ | |
AttributesStyle(defaultFont: self.defaultFont ?? .preferredFont(forTextStyle: .body), | |
// | |
defaultParagraphStyle: self.defaultParagraphStyle ?? .default, | |
// | |
defaultForegroundColor: self.defaultForegroundColor, | |
defaultUnderlineStyle: self.defaultUnderlineStyle) | |
} | |
public func attributes(basedOn: Attributes = [:]) -> Attributes | |
{ | |
basedOn.with(self) | |
} | |
} | |
public func with(_ attributesStyle: AttributesStyle, in range: NSRange? = nil) -> NSAttributedString | |
{ | |
let result = NSMutableAttributedString() | |
let replacementRange = range ?? NSRange(self.string.startIndex..., in: self.string) | |
if replacementRange.location > 0 | |
{ | |
result.append(self.attributedSubstring(from: NSRange(location: 0, length: replacementRange.location))) | |
} | |
self.enumerateAttributes(in: replacementRange, options: []) | |
{ | |
rangeAttributes, range, _ in | |
let subString = (self.string as NSString).substring(with: range) | |
result.append(NSAttributedString(string: subString, attributes: rangeAttributes.with(attributesStyle))) | |
} | |
if replacementRange.upperBound < self.length | |
{ | |
result.append(self.attributedSubstring(from: NSRange(location: replacementRange.upperBound, length: self.length - replacementRange.upperBound))) | |
} | |
return result | |
} | |
} | |
/// | |
/// `AttributedStyle Operators` | |
/// | |
@inlinable public func + (left: NSAttributedString.AttributesStyle, right: NSAttributedString.AttributesStyle) -> NSAttributedString.AttributesStyle | |
{ | |
return left.with(right) | |
} | |
@discardableResult | |
@inlinable public func += (left: inout NSAttributedString.AttributesStyle, right: NSAttributedString.AttributesStyle) -> NSAttributedString.AttributesStyle | |
{ | |
left = left + right | |
return left | |
} | |
@inlinable public func + (left: NSAttributedString.Attributes, right: NSAttributedString.AttributesStyle) -> NSAttributedString.Attributes | |
{ | |
return left.with(right) | |
} | |
@inlinable public func += (left: inout NSAttributedString.Attributes, right: NSAttributedString.AttributesStyle) -> NSAttributedString.Attributes | |
{ | |
left = left + right | |
return left | |
} | |
/// | |
/// `NSAttributedString Operators` | |
/// | |
@inlinable public func + (left: String, right: NSAttributedString.AttributesStyle) -> NSAttributedString | |
{ | |
return NSAttributedString(string: left).with(right) | |
} | |
@inlinable public func + (left: NSAttributedString, right: NSAttributedString.AttributesStyle) -> NSAttributedString | |
{ | |
return left.with(right) | |
} | |
@discardableResult | |
@inlinable public func += (left: inout NSAttributedString, right: NSAttributedString.AttributesStyle) -> NSAttributedString | |
{ | |
left = left + right | |
return left | |
} | |
@inlinable public func + (left: NSAttributedString, right: (style: NSAttributedString.AttributesStyle, range: NSRange)) -> NSAttributedString | |
{ | |
return left.with(right.style, in: right.range) | |
} | |
@discardableResult | |
@inlinable public func += (left: inout NSAttributedString, right: (style: NSAttributedString.AttributesStyle, range: NSRange)) -> NSAttributedString | |
{ | |
left = left + right | |
return left | |
} | |
// | |
// π‘π’π Playground | |
// | |
class ExampleViewController : UIViewController | |
{ | |
override func loadView() | |
{ | |
let view = UIView(frame: .zero) | |
// | |
view.backgroundColor = .white | |
let stack = UIStackView(frame: .zero) | |
stack.axis = .vertical | |
// | |
stack.spacing = 10 | |
stack.layoutMargins = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) | |
stack.isLayoutMarginsRelativeArrangement = true | |
exampleLines() | |
/// | |
/// `Template` | |
/// | |
.map | |
{ | |
let label = UILabel() | |
// | |
label.numberOfLines = 0 | |
// | |
label.attributedText = $0 | |
return label | |
} | |
/// | |
/// `Build` | |
/// | |
.forEach | |
{ | |
stack.addArrangedSubview($0) | |
} | |
stack.addArrangedSubview(UIView()) // Spacer | |
stack.autoresizingMask = [.flexibleWidth, .flexibleHeight] | |
view.addSubview(stack) | |
self.view = view | |
} | |
} | |
// | |
PlaygroundPage.current.liveView = ExampleViewController() |
This file contains hidden or 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
// | |
// π» More examples | |
// | |
func exampleLines() -> [NSAttributedString] | |
{ | |
let base = NSAttributedString.AttributesStyle().defaults() /// π¦ base/defaults style is required to process the `fragments`. | |
let spacingStyle = NSAttributedString.AttributesStyle(paragraphStyleFragment: .init()) | |
let headerStyle = NSAttributedString.AttributesStyle(foregroundColor: .gray) | |
let codeStyle = spacingStyle + NSAttributedString.AttributesStyle( | |
// | |
fontFragment: .init(family: "Menlo", | |
size: 11), | |
paragraphStyleFragment: .init(lineHeightMultiple: 0.8), | |
foregroundColor: .darkGray) | |
let style1 = NSAttributedString.AttributesStyle(foregroundColor: .red) | |
let style3 = NSAttributedString.AttributesStyle(underlineStyle: .double) | |
let fontSizeStyle = NSAttributedString.AttributesStyle(fontFragment: .init(size: 40), foregroundColor: .blue) | |
let fontBoldStyle = NSAttributedString.AttributesStyle(fontFragment: .init(addSymbolicTraits: [.traitItalic])) | |
let fontThinStyle = NSAttributedString.AttributesStyle(fontFragment: .init(weight: .thin)) | |
let existingAttributedString = NSAttributedString(string: "Font has been pre-defined", attributes: [.font : UIFont.preferredFont(forTextStyle: .headline)]) | |
return | |
[ | |
"\nexistingAttributedString looks like:" | |
+ base | |
+ headerStyle | |
, | |
existingAttributedString | |
, | |
"\nApply some styles:" | |
+ base | |
+ headerStyle | |
, | |
""" | |
existingAttributedString | |
+ (style: fontSizeStyle, range: NSRange(location: 5, length: 3)) | |
+ (style: fontBoldStyle, range: NSRange(location: 14, length: 3)) | |
+ (style: fontThinStyle, range: NSRange(location: 9, length: 3)) | |
""" + base + codeStyle | |
, | |
"\nNow it looks like:" | |
+ base | |
+ headerStyle | |
, | |
existingAttributedString | |
+ (style: fontSizeStyle, range: NSRange(location: 5, length: 3)) | |
+ (style: fontBoldStyle, range: NSRange(location: 14, length: 3)) | |
+ (style: fontThinStyle, range: NSRange(location: 9, length: 3)) | |
] | |
} |
More Examples
func exampleLines() -> [NSAttributedString]
{
let base = NSAttributedString.AttributesStyle().defaults() /// π¦ base/defaults style is required to process the `fragments`.
let spacingStyle = NSAttributedString.AttributesStyle(paragraphStyleFragment: .init())
let headerStyle = NSAttributedString.AttributesStyle(foregroundColor: .gray)
let codeStyle = spacingStyle + NSAttributedString.AttributesStyle(
//
fontFragment: .init(family: "Menlo",
size: 11),
paragraphStyleFragment: .init(lineHeightMultiple: 0.8),
foregroundColor: .darkGray)
let style1 = NSAttributedString.AttributesStyle(foregroundColor: .red)
let style3 = NSAttributedString.AttributesStyle(underlineStyle: .double)
let fontSizeStyle = NSAttributedString.AttributesStyle(fontFragment: .init(size: 40), foregroundColor: .blue)
let fontItalicStyle = NSAttributedString.AttributesStyle(fontFragment: .init(addSymbolicTraits: [.traitItalic]))
let fontThinStyle = NSAttributedString.AttributesStyle(fontFragment: .init(weight: .thin))
let existingAttributedString = NSAttributedString(string: "Font has been pre-defined", attributes: [.font : UIFont.preferredFont(forTextStyle: .headline)])
return
[
"\n The String existingAttributedString looks like:"
+ base
+ headerStyle
,
existingAttributedString
,
"\nApply some styles:"
+ base
+ headerStyle
,
"""
existingAttributedString
+ (style: fontSizeStyle, range: NSRange(location: 5, length: 3))
+ (style: fontItalicStyle, range: NSRange(location: 14, length: 3))
+ (style: fontThinStyle, range: NSRange(location: 9, length: 3))
""" + base + codeStyle
,
"\nNow it looks like:"
+ base
+ headerStyle
,
existingAttributedString
+ (style: fontSizeStyle, range: NSRange(location: 5, length: 3))
+ (style: fontItalicStyle, range: NSRange(location: 14, length: 3))
+ (style: fontThinStyle, range: NSRange(location: 9, length: 3))
]
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The Example π...