AttributedString: Making Text More Beautiful Than Ever

Published on

Get weekly handpicked updates on Swift and SwiftUI!

At WWDC 2021, Apple introduced a long-awaited feature for developers - AttributedString, which means Swift developers no longer need to use Objective-C-based NSAttributedString to create styled text. This article will provide a comprehensive introduction to AttributedString and demonstrate how to create custom attributes.

First Impression

AttributedString is a string with attributes for a single character or character range. Attributes provide features such as visual styles for display, accessibility guidance, and hyperlink data for linking between data sources.

The following code will generate an attributed string that contains bold and hyperlinked text.

Swift
var attributedString = AttributedString("Please visit Zhouzi's blog")
let zhouzi = attributedString.range(of: "Zhouzi")!  // Get the range of "Zhouzi"
attributedString[zhouzi].inlinePresentationIntent = .stronglyEmphasized // Set the attribute - bold
let blog = attributedString.range(of: "blog")!
attributedString[blog].link = URL(string: "<https://fatbobman.com>")! // Set the attribute - hyperlink

https://cdn.fatbobman.com/image-20211007165456612.png

Before WWDC 2021, SwiftUI did not provide support for attributed strings. If we wanted to display text with rich styles, we would usually use one of the following three methods:

  • Wrap UIKit or AppKit controls into SwiftUI controls and display NSAttributedString in them
  • Convert NSAttributedString into corresponding SwiftUI layout code through code
  • Display using native SwiftUI control combinations

With the changes in SwiftUI versions, there are constantly increasing means available (without using NSAttributedString):

https://cdn.fatbobman.com/image-20211006163659029.png

SwiftUI 1.0

Swift
    @ViewBuilder
    var helloView:some View{
        HStack(alignment:.lastTextBaseline, spacing:0){
            Text("Hello").font(.title).foregroundColor(.red)
            Text(" world").font(.callout).foregroundColor(.cyan)
        }
    }

SwiftUI 2.0

SwiftUI 2.0 enhanced Text’s functionality, allowing us to merge different Texts to display them together using +.

Swift
    var helloText:Text {
        Text("Hello").font(.title).foregroundColor(.red) + Text(" world").font(.callout).foregroundColor(.cyan)
    }

SwiftUI 3.0

In addition to the above methods, Text now has native support for AttributedString.

Swift
    var helloAttributedString:AttributedString {
        var hello = AttributedString("Hello")
        hello.font = .title.bold()
        hello.foregroundColor = .red
        var world = AttributedString(" world")
        world.font = .callout
        world.foregroundColor = .cyan
        return hello + world
    }

        Text(helloAttributedString)

Simply looking at the above examples, you may not see the advantages of AttributedString. I believe that with continued reading of this article, you will find that AttributedString can achieve many functions and effects that were previously impossible.

AttributedString vs NSAttributedString

AttributedString can basically be seen as the Swift implementation of NSAttributedString, and there is not much difference in functionality and internal logic between the two. However, due to differences in formation time, core code language, etc., there are still many differences between them. This section will compare them from multiple aspects.

Type

AttributedString is a value type, which is also the biggest difference between it and NSAttributedString (reference type) constructed by Objective-C. This means that it can be passed, copied, and changed like other values through Swift’s value semantics.

NSAttributedString requires different definitions for mutable or immutable

Swift
let hello = NSMutableAttributedString("hello")
let world = NSAttributedString(" world")
hello.append(world)

AttributedString

Swift
var hello = AttributedString("hello")
let world = AttributedString(" world")
hello.append(world)

Safety

In AttributedString, it is necessary to use Swift’s dot or key syntax to access attributes by name, which not only ensures type safety, but also has the advantage of compile-time checking.

AttributedString rarely uses the property access method of NSAttributedString to greatly reduce the chance of errors:

Swift
// Possible type mismatch
let attributes: [NSAttributedString.Key: Any] = [
    .font: UIFont.systemFont(ofSize: 72),
    .foregroundColor: UIColor.white,
]

Localization Support

AttributedString provides native support for localized strings and can add specific properties to them.

Swift
var localizableString = AttributedString(localized: "Hello \(Date.now,format: .dateTime) world", locale: Locale(identifier: "zh-cn"), option: .applyReplacementIndexAttribute)

Formatter Support

The new Formatter API introduced in WWDC 2021 fully supports formatting output for AttributedString types. We can easily achieve tasks that were previously impossible.

Swift
var dateString: AttributedString {
        var attributedString = Date.now.formatted(.dateTime
            .hour()
            .minute()
            .weekday()
            .attributed
        )
        let weekContainer = AttributeContainer()
            .dateField(.weekday)
        let colorContainer = AttributeContainer()
            .foregroundColor(.red)
        attributedString.replaceAttributes(weekContainer, with: colorContainer)
        return attributedString
}

Text(dateString)

https://cdn.fatbobman.com/image-20211006183053713.png

For more examples of the new Formatter API working with AttributedString, please refer to WWDC 2021’s “New and Old Comparison and Customization of the New Formatter API.”

SwiftUI Integration

The Text component in SwiftUI natively supports AttributedString, which improves a long-standing pain point for SwiftUI (although TextField and TextEdit still do not support it).

AttributedString provides available properties for three frameworks: SwiftUI, UIKit, and AppKit. UIKit or AppKit controls can also render AttributedString (after conversion).

Supported File Formats

Currently, AttributedString only has the ability to parse Markdown-format text. There is still a big gap compared to NSAttributedString’s support for Markdown, RTF, DOC, and HTML.

Conversion

Apple provides the ability to convert between AttributedString and NSAttributedString.

Swift
// AttributedString -> NSAttributedString
let nsString = NSMutableAttributedString("hello")
var attributedString = AttributedString(nsString)

// NSAttribuedString -> AttributedString
var attString = AttributedString("hello")
attString.uiKit.foregroundColor = .red
let nsString1 = NSAttributedString(attString)

Developers can take full advantage of the strengths of both to develop, for example:

  • Parse HTML with NSAttributedString, then convert it to AttributedString
  • Create a type-safe string with AttributedString and convert it to NSAttributedString for display

Basics

In this section, we will introduce some important concepts in AttributedString and demonstrate more usage through code snippets.

AttributedStringKey

AttributedStringKey defines the attribute name and type in AttributedString. Using dot syntax or KeyPath, we can access them in a type-safe manner.

Swift
var string = AttributedString("hello world")
// Using dot syntax
string.font = .callout
let font = string.font

// Using KeyPath
let font = string[keyPath:\.font]

In addition to using a large number of pre-defined system attributes, we can also create our own attributes, for example:

Swift
enum OutlineColorAttribute : AttributedStringKey {
    typealias Value = Color // Attribute type
    static let name = "OutlineColor" // Attribute name
}

string.outlineColor = .blue

We can access the properties of AttributedString, AttributedSubString, AttributeContainer, and AttributedString.Runs.Run using dot syntax or KeyPath. Refer to other code snippets in this article for more usage.

AttributeContainer

AttributeContainer is a container for attributes. By configuring the container, we can set, replace, and merge a large number of attributes for a string (or fragment) at once.

Setting Attributes

Swift
var attributedString = AttributedString("Swift")
string.foregroundColor = .red

var container = AttributeContainer()
container.inlinePresentationIntent = .strikethrough
container.font = .caption
container.backgroundColor = .pink
container.foregroundColor = .green //Will override the original red

attributedString.setAttributes(container) // attributedString now has four attribute contents

Replacing Attributes

Swift
var container = AttributeContainer()
container.inlinePresentationIntent = .strikethrough
container.font = .caption
container.backgroundColor = .pink
container.foregroundColor = .green
attributedString.setAttributes(container)
// At this point, attributedString has four attribute contents: font, backgroundColor, foregroundColor, inlinePresentationIntent

// Attributes to be replaced
var container1 = AttributeContainer()
container1.foregroundColor = .green
container1.font = .caption

// Attributes to replace with
var container2 = AttributeContainer()
container2.link = URL(string: "<https://www.swift.org>")

// All property key-value pairs in container1 that match will be replaced, for example, if container1's foregroundColor is .red, it will not be replaced
attributedString.replaceAttributes(container1, with: container2)
// After replacement, attributedString has three attribute contents: backgroundColor, inlinePresentationIntent, link

Merging Attributes

Swift
var container = AttributeContainer()
container.inlinePresentationIntent = .strikethrough
container.font = .caption
container.backgroundColor = .pink
container.foregroundColor = .green
attributedString.setAttributes(container)
// At this point, attributedString has four attribute contents: font, backgroundColor, foregroundColor, inlinePresentationIntent

var container2 = AttributeContainer()
container2.foregroundColor = .red
container2.link = URL(string: "www.swift.org")

attributedString.mergeAttributes(container2,mergePolicy: .keepNew)
// After merging, attributedString has five attribute contents: font, backgroundColor, foregroundColor, inlinePresentationIntent, and link
// foreground is .red
// When attributes conflict, use mergePolicy to choose the merge strategy: .keepNew (default) or .keepCurrent

AttributeScope

AttributeScope is a collection of attributes defined by the system framework, which defines a set of attributes suitable for a specific domain. This makes it easier to manage and also solves the problem of inconsistent attribute type for the same attribute name across different frameworks.

Currently, AttributedString provides 5 preset Scopes, which are:

  • foundation

    Contains properties related to Formatter, Markdown, URL, and language transformation.

  • swiftUI

    The properties that can be rendered under SwiftUI, such as foregroundColor, backgroundColor, font, etc. The currently supported properties are significantly less than uiKit and appKit. It is estimated that other unsupported properties will gradually be added in the future as SwiftUI provides more display support.

  • uiKit

    The properties that can be rendered under UIKit.

  • appKit

    The properties that can be rendered under AppKit.

  • accessibility

    Properties suitable for accessibility, used to improve the usability of guided access.

There are many properties with the same name in the swiftUI, uiKit, and appKit scopes (such as foregroundColor). When accessing them, the following points should be noted:

  • When Xcode cannot infer which AttributeScope to apply to a property, explicitly indicate the corresponding AttributeScope.
Swift
uiKitString.uiKit.foregroundColor = .red //UIColor
appKitString.appKit.backgroundColor = .yellow //NSColor
  • The same-named properties of the three frameworks cannot be converted to each other. If you want the string to support multi-framework display (code reuse), assign the same-named properties of different scopes separately.
Swift
attributedString.swiftUI.foregroundColor = .red
attributedString.uiKit.foregroundColor = .red
attributedString.appKit.foregroundColor = .red

// To convert it to NSAttributedString, you can convert only the specified Scope properties
let nsString = try! NSAttributedString(attributedString, including: \.uiKit)
  • In order to improve compatibility, some properties with the same function can be set in foundation.
Swift
attributedString.inlinePresentationIntent = .stronglyEmphasized // equivalent to bold
  • When defining swiftUI, uiKit, and appKit three Scopes, they have already included foundation and accessibility respectively. Therefore, even if only a single framework is specified during conversion, the properties of foundation and accessibility can also be converted normally. It is best to follow this principle when customizing Scopes.
Swift
let nsString = try! NSAttributedString(attributedString, including: \.appKit)
// The properties belonging to foundation and accessibility in attributedString will also be converted together.

Views

In the attributed string, attributes and text can be accessed independently. AttributedString provides three views to allow developers to access the content they need from another dimension.

Character and UnicodeScalar views

These two views provide functionality similar to the string property of NSAttributedString, allowing developers to manipulate data in the dimension of plain text. The only difference between the two views is the type. In simple terms, you can think of CharacterView as a collection of characters, and UnicodeScalarView as a collection of Unicode scalars.

String length

Swift
var attributedString = AttributedString("Swift")
attributedString.characters.count // 5

Length 2

Swift
let attributedString = AttributedString("hello 👩🏽‍🦳")
attributedString.characters.count // 7
attributedString.unicodeScalars.count // 10

Convert to string

Swift
String(attributedString.characters) // "Swift"

Replace String

var attributedString = AttributedString("hello world")
let range = attributedString.range(of: "hello")!
attributedString.characters.replaceSubrange(range, with: "good")
// good world, the replaced "good" still retains all the attributes of the original "hello" position

Runs View

The attribute view of the AttributedString. Each Run corresponds to a string segment with identical attributes. Use the for-in syntax to iterate over the runs property of the AttributedString.

Only One Run

All character attributes in the entire attributed string are consistent.

Swift
let attributedString = AttribuedString("Core Data")
print(attributedString)
// Core Data {}
print(attributedString.runs.count) // 1

Two Runs

Attribute string coreData has two Runs because the attributes of the two fragments, Core and Data, are not the same.

Swift
var coreData = AttributedString("Core")
coreData.font = .title
coreData.foregroundColor = .green
coreData.append(AttributedString(" Data"))

for run in coreData.runs { //runs.count = 2
    print(run)
}

// Core {
//          SwiftUI.Font = Font(provider: SwiftUI.(unknown context at $7fff5cd3a0a0).FontBox<SwiftUI.Font.(unknown context at $7fff5cd66db0).TextStyleProvider>)
//          SwiftUI.ForegroundColor = green
//          }
// Data {}

Multiple Runs

Swift
var multiRunString = AttributedString("The attributed runs of the attributed string, as a view into the underlying string.")
while let range = multiRunString.range(of: "attributed") {
    multiRunString.characters.replaceSubrange(range, with: "attributed".uppercased())
    multiRunString[range].inlinePresentationIntent = .stronglyEmphasized
}
var n = 0
for run in multiRunString.runs {
    n += 1
}
// n = 5

Final output: The ATTRIBUTED runs of the ATTRIBUTED string, as a view into the underlying string.

Using Range of Run to Set Attributes

Swift
// Continue to use the multiRunString from previous example
// Set all non-strongly emphasized characters to yellow color
for run in multiRunString.runs {
    guard run.inlinePresentationIntent != .stronglyEmphasized else {continue}
    multiRunString[run.range].foregroundColor = .yellow
}

Getting Specific Attributes through Runs

Swift
// Change the text that is yellow and strongly emphasized to red color
for (color,intent,range) in multiRunString.runs[\.foregroundColor,\.inlinePresentationIntent] {
    if color == .yellow && intent == .stronglyEmphasized {
        multiRunString[range].foregroundColor = .red
    }
}

Collecting All Used Attributes through Run’s Attributes

Swift
var totalKeysContainer = AttributeContainer()
for run in multiRunString.runs{
    let container = run.attributes
    totalKeysContainer.merge(container)
}

Using the Runs view makes it easy to get the necessary information from many attributes.

Achieve similar effects without using the Runs view

Swift
multiRunString.transformingAttributes(\.foregroundColor,\.font){ color,font in
    if color.value == .yellow && font.value == .title {
        multiRunString[color.range].backgroundColor = .green
    }
}

Although the Runs view is not directly called, the timing of the call of the transformingAttributes closure is consistent with that of Runs. transformingAttributes supports up to 5 properties.

Range

In the code before this article, Range has been used multiple times to access or modify the attributes of the attributed string content.

There are two ways to modify the attributes of local content in an attributed string:

  • Through Range
  • Through AttributedContainer

Get Range by keyword

Swift
// Search backward from the end of the attributed string and return the first range that satisfies the keyword (case insensitive)
if let range = multiRunString.range(of: "Attributed", options: [.backwards, .caseInsensitive]) {
    multiRunString[range].link = URL(string: "<https://www.apple.com>")
}

Get Range through Runs or transformingAttributes

Runs or transformingAttributes has been used repeatedly in the previous examples.

Get Range through this article view

Swift
if let lowBound = multiRunString.characters.firstIndex(of: "r"),
   let upperBound = multiRunString.characters.firstIndex(of: ","),
   lowBound < upperBound
{
    multiRunString[lowBound...upperBound].foregroundColor = .brown
}

Localization

Create localized attributed strings

Swift
// Localizable Chinese
"hello" = "你好";
// Localizable English
"hello" = "hello";

let attributedString = AttributedString(localized: "hello")

In English and Chinese environments, they will be displayed as hello and 你好 respectively.

At present, localized AttributedString can only be displayed in the language currently set in the system and cannot be specified as a specific language.

Swift
var hello = AttributedString(localized: "hello")
if let range = hello.range(of: "h") {
    hello[range].foregroundColor = .red
}

The text content of the localized string will change with the system language. The above code will not be able to obtain the range in a Chinese environment. Adjustments need to be made for different languages.

replacementIndex

You can set an index for the interpolation content of a localized string (via applyReplacementIndexAttribute) to facilitate searching in localized content.

Swift
// Localizable Chinese
"world %@ %@" = "%@ 世界 %@";
// Localizable English
"world %@ %@" = "world %@ %@";

var world = AttributedString(localized: "world \("👍") \("🥩")",options: .applyReplacementIndexAttribute) // When creating an attributed string, the index will be set in the order of interpolation, 👍 index == 1 🥩 index == 2

for (index,range) in world.runs[\.replacementIndex] {
    switch index {
        case 1:
            world[range].baselineOffset = 20
            world[range].font = .title
        case 2:
            world[range].backgroundColor = .blue
        default:
            world[range].inlinePresentationIntent = .strikethrough
    }
}

In Chinese and English environments, respectively:

https://cdn.fatbobman.com/image-20211007083048701-3566650.png

https://cdn.fatbobman.com/image-20211007083115822.png

Using locale to set Formatter in string interpolation

Swift
 AttributedString(localized: "\(Date.now, format: Date.FormatStyle(date: .long))", locale: Locale(identifier: "zh-cn"))
// Will display "2021年10月7日" even in an English environment

Generating attributed strings with Formatter

Swift
        var dateString = Date.now.formatted(.dateTime.year().month().day().attributed)
        dateString.transformingAttributes(\.dateField) { dateField in
            switch dateField.value {
            case .month:
                dateString[dateField.range].foregroundColor = .red
            case .day:
                dateString[dateField.range].foregroundColor = .green
            case .year:
                dateString[dateField.range].foregroundColor = .blue
            default:
                break
            }
        }

https://cdn.fatbobman.com/image-20211007084630319.png

Markdown Symbols

Starting from SwiftUI 3.0, Text has provided support for some Markdown tags. Similar functionality is also available in localized attributed strings, which will set corresponding attributes in the string, providing greater flexibility.

Swift
var markdownString = AttributedString(localized: "**Hello** ~world~ _!_")
for (inlineIntent,range) in markdownString.runs[\.inlinePresentationIntent] {
    guard let inlineIntent = inlineIntent else {continue}
    switch inlineIntent{
        case .stronglyEmphasized:
            markdownString[range].foregroundColor = .red
        case .emphasized:
            markdownString[range].foregroundColor = .green
        case .strikethrough:
            markdownString[range].foregroundColor = .blue
        default:
            break
    }
}

https://cdn.fatbobman.com/image-20211007085859409.png

Markdown Parsing

AttributedString not only supports partial Markdown tags in localized strings, but also provides a complete Markdown parser.

It supports parsing Markdown text content from String, Data, or URL.

For example:

Swift
let mdString = try! AttributedString(markdown: "# Title\n**hello**\n")
print(mdString)

// Parsing results
Title {
    NSPresentationIntent = [header 1 (id 1)]
}
hello {
    NSInlinePresentationIntent = NSInlinePresentationIntent(rawValue: 2)
    NSPresentationIntent = [paragraph (id 2)]
}

After parsing, the text style and tags will be set in inlinePresentationIntent and presentationIntent.

  • inlinePresentationIntent

    Character properties: such as bold, italic, code, quote, etc.

  • presentationIntent

    Paragraph attributes: such as paragraph, table, list, etc. In a Run, presentationIntent may have multiple contents, which can be obtained using component.

README.md

Swift
#  Hello

## Header2

hello **world**

* first
* second

> test `print("hello world")`

| row1 | row2 |
| ---- | ---- |
| 34   | 135  |

[新Formatter介绍](/posts/newFormatter/)

Code analysis:

Swift
let url = Bundle.main.url(forResource: "README", withExtension: "md")!
var markdownString = try! AttributedString(contentsOf: url,baseURL: URL(string: "<https://fatbobman.com>"))

Result after analysis (excerpt):

Swift
Hello {
    NSPresentationIntent = [header 1 (id 1)]
}
Header2 {
    NSPresentationIntent = [header 2 (id 2)]
}
first {
    NSPresentationIntent = [paragraph (id 6), listItem 1 (id 5), unorderedList (id 4)]
}

test  {
    NSPresentationIntent = [paragraph (id 10), blockQuote (id 9)]
}
print("hello world") {
    NSPresentationIntent = [paragraph (id 10), blockQuote (id 9)]
    NSInlinePresentationIntent = NSInlinePresentationIntent(rawValue: 4)
}
row1 {
    NSPresentationIntent = [tableCell 0 (id 13), tableHeaderRow (id 12), table [Foundation.PresentationIntent.TableColumn(alignment: Foundation.PresentationIntent.TableColumn.Alignment.left), Foundation.PresentationIntent.TableColumn(alignment: Foundation.PresentationIntent.TableColumn.Alignment.left)] (id 11)]
}
row2 {
    NSPresentationIntent = [tableCell 1 (id 14), tableHeaderRow (id 12), table [Foundation.PresentationIntent.TableColumn(alignment: Foundation.PresentationIntent.TableColumn.Alignment.left), Foundation.PresentationIntent.TableColumn(alignment: Foundation.PresentationIntent.TableColumn.Alignment.left)] (id 11)]
}
新Formatter介绍 {
    NSPresentationIntent = [paragraph (id 18)]
    NSLink = /posts/newFormatter/ -- https://fatbobman.com
}

The parsed content includes paragraph properties, header numbers, table column and row numbers, alignment, and so on. Other information such as indentation and numbering can be handled by enumerating associated values in the code.

The approximate code is as follows:

Swift
for run in markdownString.runs {
    if let inlinePresentationIntent = run.inlinePresentationIntent {
        switch inlinePresentationIntent {
        case .strikethrough:
            print("strikethrough")
        case .stronglyEmphasized:
            print("bold")
        default:
            break
        }
    }
    if let presentationIntent = run.presentationIntent {
        for component in presentationIntent.components {
            switch component.kind{
                case .codeBlock(let languageHint):
                    print(languageHint)
                case .header(let level):
                    print(level)
                case .paragraph:
                    let paragraphID = component.identity
                default:
                    break
            }
        }
    }
}

SwiftUI does not support rendering of attached information in presentationIntent. If you want to achieve the desired display effect, please write your own code for visual style setting.

Custom Attributes

Using custom attributes not only benefits developers in creating attribute strings that better meet their own requirements, but also reduces the coupling between information and code by adding custom attribute information to Markdown text, thereby improving flexibility.

The basic process for creating custom attributes is:

  • Create custom AttributedStringKey

    Create a data type that conforms to the Attributed protocol for each attribute that needs to be added.

  • Create custom AttributeScope and extend AttributeScopes

    Create your own scope and add all custom attributes to it. In order to facilitate the use of custom attribute sets in situations where the scope needs to be specified, it is recommended to nest the required system framework scopes (SwiftUI, UIKit, AppKit) in the custom scope. And add the custom scope to AttributeScopes.

  • Extend AttributeDynamicLookup (supports dot syntax)

    Create subscript methods that conform to the custom scope in AttributeDynamicLookup. Provide dynamic support for dot syntax and KeyPath.

Example 1: Creating an id attribute

In this example, we will create an attribute named “id”.

Swift
struct MyIDKey:AttributedStringKey {
    typealias Value = Int // The type of the attribute's content. The type needs to be Hashable.
    static var name: String = "id" // The name of the attribute stored in the attribute string.
}

extension AttributeScopes{
    public struct MyScope:AttributeScope{
        let id:MyIDKey  // The name called by dot syntax.
        let swiftUI:SwiftUIAttributes // Include the system framework SwiftUI in MyScope.
    }

    var myScope:MyScope.Type{
        MyScope.self
    }
}

extension AttributeDynamicLookup{
    subscript<T>(dynamicMember keyPath:KeyPath<AttributeScopes.MyScope,T>) -> T where T:AttributedStringKey {
        self[T.self]
    }
}

Usage

Swift
var attribtedString = AttributedString("hello world")
attribtedString.id = 34
print(attribtedString)

// Output
hello world {
    id = 34
}

Example 2: Creating an enumerated attribute and supporting Markdown parsing

If we want the attributes we create to be parsed in Markdown text, we need to make our custom attributes conform to CodeableAttributedStringKey and MarkdownDecodableAttributedStringKye.

Swift
// The data type of the custom attribute can be anything as long as it conforms to the necessary protocols.
enum PriorityKey: CodableAttributedStringKey, MarkdownDecodableAttributedStringKey {
    public enum Priority: String, Codable { // To decode in Markdown, set the raw type to String and conform to Codable.
        case low
        case normal
        case high
    }

    static var name: String = "priority"
    typealias Value = Priority
}

extension AttributeScopes {
    public struct MyScope: AttributeScope {
        let id: MyIDKey
        let priority: PriorityKey // Add the newly created Key to the custom Scope.
        let swiftUI: SwiftUIAttributes
    }

    var myScope: MyScope.Type {
        MyScope.self
    }
}

In Markdown, use ^[text](attribute_name: attribute_value) to mark custom attributes.

Call

Swift
// When parsing custom properties in Markdown text, specify the Scope.
var attributedString = AttributedString(localized: "^[hello world](priority:'low')",including: \.myScope)
print(attributedString)

// Output
hello world {
    priority = low
    NSLanguage = en
}

Example 3: Creating Properties with Multiple Parameters

Swift
enum SizeKey:CodableAttributedStringKey,MarkdownDecodableAttributedStringKey{
    public struct Size:Codable,Hashable{
        let width:Double
        let height:Double
    }

    static var name: String = "size"
    typealias Value = Size
}

// Add to Scope
let size:SizeKey

Call

Swift
// Add multiple parameters within {}
let attributedString = AttributedString(localized: "^[hello world](size:{width:343.3,height:200.3},priority:'high')",including: \.myScope)
print(attributedString)

// Output
hello world {
    priority = high
    size = Size(width: 343.3, height: 200.3)
    NSLanguage = en
}

In the WWDC 2021 New Formatter API article, there are also cases of using custom properties in Formatters.

Conclusion

Before AttributedString, most developers mainly used attributed strings to describe text display styles. With the ability to add custom properties in Markdown text, it is believed that developers will soon expand the use of AttributedString and apply it to more scenarios.

I hope this article is helpful to you.

Weekly Swift & SwiftUI insights, delivered every Monday night. Join developers worldwide.
Easy unsubscribe, zero spam guaranteed