Skip to content

Instantly share code, notes, and snippets.

@raygun101
Last active February 9, 2022 06:09
Show Gist options
  • Save raygun101/ceb8733d6472cab06265836d4ca28d73 to your computer and use it in GitHub Desktop.
Save raygun101/ceb8733d6472cab06265836d4ca28d73 to your computer and use it in GitHub Desktop.
🍰 Layered Cakewalk [Swift] - AttributesStyle
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()
//
// πŸ‘» 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))
]
}
@raygun101
Copy link
Author

raygun101 commented Sep 14, 2020

The Example πŸš—...

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))
        ]
}

image

@raygun101
Copy link
Author

More Examples

image

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