AttributedString Attribute Scopes

0
50

In 2021 we got a new Foundation type that represents a string with attributes: AttributedString. Attributes on ranges of the string can represent visual styles, accessibility features, link data and more. In contrast with the old NSAttributedStringnew AttributedString provides type-safe API, which means you can not assign a wrong type to an attribute by mistake.

AttributedString can be used in a variety of contexts and its attributes are defined in separate collections nested under AttributeScopes. System frameworks such as Foundation, UIKit, AppKit and SwiftUI define their own scopes.

Foundation attributes

Foundation defines attributes listed under AttributeScopes.FoundationAttributes.

We can think of Foundation attributes as lower-level scope, it consists of common attributes that are defined on the Foundation level and included in scopes defined by UI frameworks such as UIKit, AppKit and SwiftUI.

One of the most commonly used Foundation attributes is LinkAttribute also available via link property.

There are a few different ways we can assign a link attributes to our AttributedString.

The shortest way is to use link property and let it infer the scope on its own.

var attributedString = AttributedString("Visit out site")
attributedString.link = URL(string: "http://example.com")

We can also be more explicit and indicate that it’s the Foundation scope link that we are assigning. In this case it’s not necessary since link attribute with value of type URL only exists in the Foundation scope. But there might be situations where scopes are ambiguous.

var attributedString = AttributedString("Visit out site")
attributedString.foundation.link = URL(string: "http://example.com")

The link property is a convenince and we can also assign the attribute using its full type as subscript. Again this is not necessary in our case, but good to know all the available options.

var attributedString = AttributedString("Visit out site")
attributedString[
    AttributeScopes.FoundationAttributes.LinkAttribute.self
] = URL(string: "http://example.com")

Foundation scope also includes Markdown attributes such as InlinePresentationIntentAttribute and PresentationIntentAttribute.

We usually do not assign Markdown attributes ourselves, they are created when Foundation parses a Markdown string into an AttributedString.

We might be creating an AttributedString from a string with Markdown styles as follows. The resulting AttributedString will have presentation intent attributes for header and list items.

let string = """
# Chocolate cake recipe
1. Preheat oven to 180°C.
2. Mix together sugar, cocoa and flour.
3. Whisk the eggs.
"""

let attributedString = try! AttributedString(
    markdown: string
)

Note, that when creating a localized attributed string with Markdown, only inline presentation intent attributes will be parsed, because AttributedString.init(localized:) implicitly uses inlineOnlyPreservingWhitespace parsing option. In the example below the resulting AttributedString will contain inline presentation intent for bold text.

let string: String.LocalizationValue = "Preheat oven to **180°C**."
let attributedString = AttributedString(localized: string)

UIKit and AppKit attributes

UIKit and AppKit attribute scopes have many attributes with matching names but different value types. Attributes with types defined in UIKit such as UIColor, UIFont etc. are available on platforms that can import UIKit and attributes with types defined in AppKit such as NSColor, NSFont etc. are available on macOS. AttributeScopes.UIKitAttributes type contains the full list of UIKit attributes and AttributeScopes.AppKitAttributes contains the full list of AppKit attributes.

Foundation can infer the right type of the attribute to use when we assign the value. The following code will assign UIColor.blue as foreground color when UIKit is imported and NSColor.blue when AppKit is imported.

var attributedString = AttributedString("Hello")
attributedString.foregroundColor = .blue

Note, that when SwiftUI is imported SwiftUI attributes take priority and the resulting attribute value will be Color.blue that is defined in the SwiftUI framework.

System provided views such UILabel and NSLabel do not work with AttributedString at the moment. When we want to display our attributed text to the user, we need to convert it to the old type NSAttributedString.

var attributedString = AttributedString("Hello")
attributedString.foregroundColor = .blue

let nsAttributedString = NSAttributedString(attributedString)

let label = UILabel(
    frame: CGRect(x: 0, y: 0, width: 100, height: 100)
)
label.attributedText = nsAttributedString

SwiftUI attributes

SwiftUI framework also defines its own attributes listed in AttributeScopes.SwiftUIAttributes. They have value types that are defined in SwiftUI such as Color, Font etc.

Text view can be created with an AttributedString and display styles included in SwiftUI scope. For example, foregroundColor with value type of Color is among the supported attributes and Text view knows how to display this style, so it will color the text in blue.

var attributedString = AttributedString("Hello")
attributedString.foregroundColor = .blue

Text(attributedString)

Text view can also internally convert some attributes from UIKit and AppKit scopes and display them appropriately. Attributes that get converted by Text have equivalent values ​​in SwiftUI scope, such as foregroundColor, backgroundColor, font etc. In the following example, even if we assign UIFont to our AttributedStringSwiftUI can convert it to Font and display the string using bold font of size 50.

var attributedString = AttributedString("Hello")
attributedString.font = UIFont.boldSystemFont(ofSize: 50)

Text(attributedString)

Attributes that cannot be converted by SwiftUI because they are not present in the SwiftUI scope, such as paragraphStyle for example, will be ignored.

SwiftUI attributes always take priority over UIKit and AppKit attributes.

SwiftUI scope also includes Foundation attributes, but not all of them are recognized by SwiftUI. LinkAttribute and most of InlinePresentationIntent options are supported. You can read more on it in documentation about Text and AttributedString.

Accessibility attributes

Accessibility attributes are listed in AttributeScopes.AccessibilityAttributes and are included in UIKit, AppKit and SwiftUI scopes. These attributes can be used to improve accessibility settings for particular ranges of the string or the whole string.

For example, if we have some sort of code that we show in our app and need to differentiate between capital and small letters, we can set accessibilitySpeechSpellsOutCharacters to true on that code. This will make VoiceOver spell the code letter by letter and indicate capital letters.

var attributedString = AttributedString("TU7gbO")
attributedString.accessibilitySpeechSpellsOutCharacters = true

Text(attributedString)

Custom attributes

We can even define our own attributes and attribute scopes. This can be useful if we are building a framework or a package or just want to extend AttributedString functionality in our app.

As a simple example we are going to define our own TextCaseAttribute that can be set to lowercase or uppercase on a range of the string or the whole string.

We define TextCase enum that conforms to Hashable so that it can be used as the value for our attribute. Then we define the attribute and include it in our custom scope.

enum TextCase: Hashable 
    case lowercase
    case uppercase


struct TextCaseAttribute: AttributedStringKey 
    typealias Value = TextCase
    static var name = "TextCaseAttribute"


struct ExtendedTextAttributes: AttributeScope 
    let textCase: TextCaseAttribute

To be able to access our attribute via a property on AttributedStringwe need to extend AttributeDynamicLookup with our subscript.

extension AttributeDynamicLookup 
    subscript<T: AttributedStringKey>(
        dynamicMember keyPath: KeyPath<ExtendedTextAttributes, T>
    ) -> T 
        get  self[T.self] 
    


var attributedString = AttributedString("Hello")
attributedString.textCase = .uppercase

Note, that none of Apple UI frameworks know how to deal with this attribute. If we want to display it to the users in UIKit, AppKit or SwiftUI, we need to pre-process the AttributedString beforehand and uppercase the affected text ourselves.

For updates about the blog and development tips follow us on Twitter @nilcoalescing.

Source

LEAVE A REPLY

Please enter your comment!
Please enter your name here