AttributedString — Making Text More Beautiful Than Ever

fatbobman ( 东坡肘子)
15 min readApr 7, 2023


Photo by Brett Jordan on Unsplash

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.

A Chinese version of this post is available here.

Don’t miss out on the latest updates and excellent articles about Swift, SwiftUI, Core Data, and SwiftData. Subscribe to fatbobman’s Swift Weekly and receive weekly insights and valuable content directly to your inbox.

In light of the fact that my blog, Fatbobman’s Blog, now offers all articles in English, starting from April 1, 2024, I will no longer continue updating articles on Medium. You are cordially invited to visit my blog for more content.

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.

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: "<>")! // Set the attribute - hyperlink

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):

SwiftUI 1.0

var helloView:some View{
HStack(alignment:.lastTextBaseline, spacing:0){
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 +.

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.

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


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.


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

let hello = NSMutableAttributedString("hello")
let world = NSAttributedString(" world")


var hello = AttributedString("hello")
let world = AttributedString(" world")


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:

// 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.

var localizableString = AttributedString(localized: "Hello \(,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.

var dateString: AttributedString {
var attributedString =
let weekContainer = AttributeContainer()
let colorContainer = AttributeContainer()
attributedString.replaceAttributes(weekContainer, with: colorContainer)
return attributedString


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.


Apple provides the ability to convert between AttributedString and NSAttributedString.

// 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


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


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

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:

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 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

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

var container = AttributeContainer()
container.inlinePresentationIntent = .strikethrough
container.font = .caption
container.backgroundColor = .pink
container.foregroundColor = .green
// 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() = URL(string: "<>")
// 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

var container = AttributeContainer()
container.inlinePresentationIntent = .strikethrough
container.font = .caption
container.backgroundColor = .pink
container.foregroundColor = .green
// At this point, attributedString has four attribute contents: font, backgroundColor, foregroundColor, inlinePresentationIntent
var container2 = AttributeContainer()
container2.foregroundColor = .red = URL(string: "")
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 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.
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.
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.
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.
let nsString = try! NSAttributedString(attributedString, including: \.appKit)
// The properties belonging to foundation and accessibility in attributedString will also be converted together.


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

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

Length 2

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

Convert to string

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.

let attributedString = AttribuedString("Core Data")
// 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.

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

for run in coreData.runs { //runs.count = 2
// Core {
// SwiftUI.Font = Font(provider: SwiftUI.(unknown context at $7fff5cd3a0a0).FontBox<SwiftUI.Font.(unknown context at $7fff5cd66db0).TextStyleProvider>)
// SwiftUI.ForegroundColor = green
// }
// Data {}

Multiple Runs

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

// 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

// 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

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

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

Achieve similar effects without using the Runs view

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.


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

// 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: "<>")

Get Range through Runs or transformingAttributes

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

Get Range through this article view

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


Create localized attributed strings

// 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.

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.


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

// 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
world[range].inlinePresentationIntent = .strikethrough

In Chinese and English environments, respectively:

Using locale to set Formatter in string interpolation

AttributedString(localized: "\(, 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

var dateString =
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

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.

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

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:

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

// 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.

#  Hello

## Header2
hello **world**
* first
* second
> test `print("hello world")`
| row1 | row2 |
| ---- | ---- |
| 34 | 135 |

Code analysis:

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

Result after analysis (excerpt):

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/ -- <>

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:

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

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”.

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{
extension AttributeDynamicLookup{
subscript<T>(dynamicMember keyPath:KeyPath<AttributeScopes.MyScope,T>) -> T where T:AttributedStringKey {


var attribtedString = AttributedString("hello world") = 34

// 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.

// 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 {

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


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

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

Example 3: Creating Properties with Multiple Parameters

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


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

// 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.


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.

If you found this article helpful or enjoyed reading it, consider making a donation to support my writing. Your contribution will help me continue creating valuable content for you.
Donate via Patreon, Buy Me aCoffee or PayPal.

Want to Connect?

@fatbobman on Twitter.



fatbobman ( 东坡肘子)

Blogger | Sharing articles at | Publisher of a weekly newsletter on Swift at http://