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.
# 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.
If you have older iOS apps and want to enhance them with modern SwiftUI features, check out my book Integrating SwiftUI into UIKit Apps. It provides detailed guidance on gradually adopting SwiftUI in your UIKit projects. Additionally, if you're eager to enhance your Swift programming skills, my latest book Swift Gems offers over a hundred advanced tips and techniques, including optimizing collections, handling strings, mastering asynchronous programming, and debugging, to take your Swift code to the next level.