Black Friday deal: 30% off our book "Integrating SwiftUI into UIKit apps"! Learn more ...Black Friday deal:30% off "Integrating SwiftUI into UIKit apps" >>

AttributedString attribute scopes

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 NSAttributedString, new AttributedString provides type-safe API, which means you can't 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.

Integrating SwiftUI into UIKit Apps by Natalia Panferova book coverIntegrating SwiftUI into UIKit Apps by Natalia Panferova book cover
Black Friday offer
Our book "Integrating SwiftUI into UIKit apps" is now 30% off!

Discover various ways to adopt SwiftUI in existing UIKit projects and take full advantage of the new iOS 16 frameworks and APIs such as Swift Charts.

The offer is active until the 1st of December.

# 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 don't 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 don't 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 AttributedString, SwiftUI 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 AttributedString, we 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'll need to pre-process the AttributedString beforehand and uppercase the affected text ourselves.