AttributedString Tutorial for Swift: Getting Started

Learn how to format text and create custom styles using iOS 15’s new AttributedString value type as you build a Markdown previewer in SwiftUI. By Ehab Yosry Amer.

4 (1) · 1 Review

Download materials
Save for later

Building great-looking apps doesn’t rely on just images — it also extends to text. Different styles in attributed strings can go a great distance to making information more appealing. In this tutorial, you’ll learn about the new AttributedString value type introduced in iOS 15 and macOS 12. You’ll also see how to leverage its capabilities, including formatting with Markdown, to do more with text in your apps.

This tutorial will cover:

  • Differences between the new AttributedString and the older NSAttributedString that’s bridged from Objective-C.
  • Formatting and styling an attributed string using Markdown.
  • The structure of an attributed string and how to alter it.
  • Creating and rendering custom attributes.
  • Encoding and decoding an attributed string and its custom attributes.
Note: This tutorial assumes some familiarity with iOS programming, which often includes working with strings. If you’re brand new, consider starting with a book like SwiftUI Apprentice or a video course like Your First iOS and SwiftUI App: An App From Scratch. Feel free to give this tutorial a go though and jump into the forum (linked below) to ask questions!

Getting Started

Download the starter project by clicking Download Materials at the top or bottom of the tutorial.

The app you’ll build, Markdown Preview, allows you to type a basic text string that it then converts to an attributed string. Then, it saves this attributed string to a library of your creation.

Start by opening MarkdownPreview.xcodeproj in the starter folder. Build and run the app to see your starting point.

Markdown Preview app start screen

The first section of the screen allows you to choose a theme. A group of themes is already included in the project but won’t have any effect yet.

Markdown Preview app Select Theme screen with four options

You’ll divide the work on this app into five parts:

  1. Converting a Markdown string to an attributed string.
  2. Applying the themes on the text without permanently changing its attributes.
  3. Creating custom attributes that can be part of your Markdown.
  4. Creating a text view that can render the new custom attributes.
  5. Saving your attributed string into a library.

AttributedString vs. NSAttributedString

Before you start working on the project, it’s worth knowing a few things about AttributedString in comparison with the older NSAttributedString. Specifically, it:

  • Is a first-class Swift citizen and takes advantage of Swift features, similar to the differences between String and NSString.
  • Is a value type, while the older NSAttributedString is a reference type.
  • Conforms to Codable. You can directly encode and decode an AttributedString object along with its attributes just like working with a normal String.
  • Has the same character-counting behavior as String.
  • Is fully localizable. You can even define styles in your text directly in the localization files!
  • Most importantly, AttributedString has full support for Markdown.

Using Markdown

Markdown is a popular markup language for formatting text. It can format whole documents — not just paragraphs. You might be surprised to learn that all the books published here on are written entirely in Markdown. :]

Note: You can learn more about Markdown’s syntax from the Markdown Cheat Sheet.

Write a Markdown string in the Raw Markdown text field, and notice that the text appears as-is in the Rendered Markdown area. The Markdown attributes aren’t translated to style the text yet.

Markdown attributes not translated to style the text.

Open MarkdownView.swift in the Views group, then go to convertMarkdown(_:). This method handles converting your raw text to an AttributedString. Your text isn’t treated as Markdown on its own if you use the standard initializer AttributedString(_:). Change the implementation of the method to:

// 1
guard var attributedString = try? AttributedString(markdown: string) else {
  // 2
  return AttributedString(string)
// 3
// 4
return attributedString

The code you added does the following:

  1. Tries to convert the raw string to an attributed string using the initializer AttributedString(markdown:).
  2. If it fails, then it creates an attributed string using the default initializer without any Markdown styling.
  3. Prints some information about the attributed string. This method is currently empty. You’ll implement it in the next section.
  4. Returns the attributed string that succeeded in the Markdown initializer.

Build and run. Enter the same Markdown string you tried before in the Raw Markdown area, and see how it appears now:

Raw Markdown with correct styling.

Examining the Structure of an AttributedString

An AttributedString object consists of characters and you can count them just like a normal string. But it also consists of AttributedString.Runs.

Runs are the parts of an AttributedString that describe what style applies to which characters in the text. Each run consists of a range for a substring and the styles applied to it. If your text is plain and has no styles, then your attributed string will consist of only one run. If your AttributedString uses a variety of styles, then it’ll be broken down into many runs. You’ll get deeper into runs shortly.

Characters and Indices

To get a better idea of what characters are, return to Views/MarkdownView.swift, then go to printStringInfo(_:). Implement it as follows:

// 1
print("The string has \(attributedString.characters.count) characters")
// 2
let characters = attributedString.characters
// 3
for char in characters {

Here’s what’s happening in the code above:

  1. Print the number of characters in the attributed string.
  2. Create a variable holding the AttributedString.CharacterView that you’ll iterate over to get the value of each character separately.
  3. Iterate over this collection and prints the value of the characters one by one.

Build and run. Enter this raw Markdown string to try it out:

This is **Bold text** and this is _italic_

You’ll see the output from printStringInfo(_:) in Xcode’s console:

The string has 36 characters

The original string is 42 letters. But when treated as Markdown, the ** and _ characters that are part of the Markdown syntax are no longer part of the actual string. They became attributes or style, not content.

Scroll up a little in the log, and you’ll find that the previous log on the character count is 37. That happened right before you entered the last _, when the string was:

This is **Bold text** and this is _italic

You hadn’t entered the closing _ for the italic syntax, so AttributedString wasn’t treating this part as italic yet. Both opening and closing characters must be present, otherwise they’re considered part of the content.

Attributed string with a missing closing character for italic style

Notice that the italic style isn’t applied, and the _ is part of the content in the attributed string.


Looking at the attributed string you entered above, you can describe it like this:

  • This is uses regular style.
  • Bold text uses bold style.
  • and this is uses regular style.
  • italic would use italic style, had it been formatted with correct Markdown.

Those parts, in order, describe the string and its attributes. If you try to merge the first and the third parts, because they have the same style, you’ll end up with some complexity in defining the order of where the regular style should be applied.

This is very similar to runs and how they describe an attributed string.

Add the following at the end of printStringInfo(_:) in Views/MarkdownView.swift:

// 1
print("The string has \(attributedString.runs.count) runs")
// 2
let runs = attributedString.runs
// 3
for run in runs {
  // 4

  // 5
  if let textStyle = run.inlinePresentationIntent {

    // 6
    if textStyle.contains(.stronglyEmphasized) {
      print("Text is Bold")
    if textStyle.contains(.emphasized) {
      print("Text is Italic")
  // 7
  } else {
    print("Text is Regular")

Here’s what’s happening in the code:

  1. Print the number of runs present in the attributed string.
  2. Create a variable for the collection of runs.
  3. Iterate over the collection.
  4. Print the description of the run.
  5. If the run has a value for the style inlinePresentationIntent, store its value for use. This value is an OptionSet. It can hold one or more values where each value is represented by a bit.
  6. If the stored value has .stronglyEmphasized as an option, then print “Text is Bold”. Or, if it has .emphasized, print “Text is Italic”.
  7. If no value is present, this means the text doesn’t have this style, so print “Text is Regular”.

Build and run. Enter the same text as before and check the log:

The string has 4 runs
This is  {
  NSPresentationIntent = [paragraph (id 1)]
Text is Regular
Bold text {
  NSPresentationIntent = [paragraph (id 1)]
  NSInlinePresentationIntent = NSInlinePresentationIntent(rawValue: 2)
Text is Bold
 and this is  {
  NSPresentationIntent = [paragraph (id 1)]
Text is Regular
italic {
  NSPresentationIntent = [paragraph (id 1)]
  NSInlinePresentationIntent = NSInlinePresentationIntent(rawValue: 1)
Text is Italic

The number of runs is four, as you broke it down earlier. By checking the details of each run, you see they also have the same structure: the text and the available styles with their values.

Notice that the bold style has the value NSInlinePresentationIntent(rawValue: 2) and the italic has NSInlinePresentationIntent(rawValue: 1). Notice the raw values. Enter the following string as the raw Markdown:

This is **Bold text** and this is _italic_ and this is **_both_**

This string has six runs. The final one has the NSInlinePresentationIntent style value of NSInlinePresentationIntent(rawValue: 3). This means it’s italic and bold. Each option is represented by a bit. So, 2^0 + 2^1 = 3. It doesn’t have to be exclusively either bold or italic.

Applying the Themes

As you can see from the details of the runs, the attributed string you created didn’t specify any font name or size. It only had bold and italic styles.

An attributed string can define a font name, size, color and many other attributes. But in this app, you’ll build it differently. You want to have a set of themes you can choose from to apply to the string. Imagine you’re creating a text editor for Markdown, and you want the user to be able to choose a theme or style before printing the document. This means the theme won’t alter the original Markdown the user typed, but the chosen theme will determine how the final document looks.

Defining Theme Styles

iOS 15 provides a way to package a group of styles together so you can apply them in bulk on an attributed string. Do this by using AttributeContainer.

Go to TextTheme.swift in the Models group and add this computed property in the enumeration:

var attributeContainer: AttributeContainer {
  var container = AttributeContainer()
  switch self {
  case .menlo:
    container.font = .custom("Menlo", size: 17, relativeTo: .body)
    container.foregroundColor = .indigo
  case .times:
    container.font = .custom("Times New Roman", size: 17, relativeTo: .body)
    container.foregroundColor =
  case .important:
    container.font = .custom("Courier New", size: 17, relativeTo: .body)
    container.backgroundColor = .yellow
  return container

This creates a container with different attributes based on the current enumeration value. Each has a different font and a foreground or background color.

Next, go to Views/MarkdownView.swift and in convertMarkdown(_:), right before calling printStringInfo(_:), add this:


This tells the attributed string you created from the Markdown to merge its attributes with the ones from the theme’s attribute container.

Build and run. Change the theme a few times and see how the results change from one theme to another.

Markdown Preview app with a theme selected, showing text with a yellow background

Creating Custom Attributes

So far, you’ve learned a lot about Swift’s new AttributedString and how you can do different things with it. But you may have a few questions like:

  • How do I define a new attribute?
  • How can I combine existing styles to create a new effect on the text?
  • What should the Markdown look like for a new attribute?
  • How will the attributed string recognize the new attribute and how it will render?

Those are all good questions. The first thing to cover is that Markdown allows adding attributes directly, like this:

Some regular text then ^[text with an attribute](theAttributeKey: 'theValue')

Some regular text then ^[text with two attributes]
  (theAttributeKey: 'theValue', otherAttributeKey: 'theOtherValue')

In the Markdown examples above, you have two custom attributes: theAttributeKey and otherAttributeKey. This is valid Markdown syntax, but for AttributedString to understand these attributes, you need to define an attribute scope.

Attribute Scopes

Attribute scopes help decode the attributes from Markdown or an encoded attributes string. Scopes are already defined for Foundation, UIKit, AppKit and SwiftUI.

When decoding the attributes, only one scope is used. The latter three include the Foundation’s scope inside them.

If you’re thinking: “Enough talking — show me how all that works!”, that’s completely understandable. :]

Create a new Swift file in the Models group named AttributeScopes.swift and add the following:

import SwiftUI

public enum CustomStyleAttributes {
  public enum Value: String, Codable {
    case boldcaps, smallitalics

  public static var name = "customStyle"

public enum CustomColorAttributes {
  public enum Value: String, Codable {
    case danger, highlight

  public static var name = "customColor"

These enumerations are the two custom attributes you’ll create. Each of them has a subtype enumeration with the allowed values and a string representation for its Key name that will appear in the Markdown:

  • CustomStyleAttributes modifies the text to make it bold and uppercase or italic and lowercase.
  • CustomColorAttributes affects colors:
    • danger adds a red background and a yellow dashed underline to the text.
    • highlight adds a yellow background and a red dotted underline to the text.

Add the following in the same file:

// 1
public extension AttributeScopes {
  // 2
  struct CustomAttributes: AttributeScope {
    let customStyle: CustomStyleAttributes
    let customColor: CustomColorAttributes
    // 3
    let swiftUI: SwiftUIAttributes
  // 4
  var customAttributes: CustomAttributes.Type { CustomAttributes.self }

// 5
public extension AttributeDynamicLookup {
  subscript<T: AttributedStringKey>(
    dynamicMember keyPath: KeyPath<AttributeScopes.CustomAttributes, T>
  ) -> T {

AttributedString can understand the custom keys in Markdown by:

  1. Creating an extension to the existing AttributeScopes type.
  2. Creating a new subtype to hold all the custom attributes you wish to use.
  3. Specifying a property that will refer to the existing attributes. Since this app is in SwiftUI, it’s SwiftUIAttributes. Alternatively, you can use FoundationAttributes, UIKitAttributes or AppKitAttributes. Otherwise, existing attributes won’t be encoded and decoded.
  4. Specifying a property that refers to the type itself.
  5. Specifying an extension on AttributeDynamicLookup with an override to subscript(dynamicMember:). This helps you refer to CustomAttributes directly as a KeyPath.

Before you try it, the first enumerations you created must conform to CodableAttributedStringKey since you’ll use them as Codable properties in the attributed string and MarkdownDecodableAttributedStringKey since you’ll use them from Markdown. Change the declaration of the enumerations to:

public enum CustomStyleAttributes: CodableAttributedStringKey, 
  MarkdownDecodableAttributedStringKey {
public enum CustomColorAttributes: CodableAttributedStringKey, 
  MarkdownDecodableAttributedStringKey {

Finally, in Views/MarkdownView.swift, change how you initialize the attributed string from Markdown:

guard var attributedString = try? AttributedString(
  markdown: string,
  including: AttributeScopes.CustomAttributes.self,
  options: AttributedString.MarkdownParsingOptions(
    allowsExtendedAttributes: true)) else {
    return AttributedString(string)

Build and run. Enter the following Markdown to test your changes:

^[BoldCaps and Danger](customStyle: 'boldcaps', customColor: 'danger'), 
^[SmallItalics and Highlighted](customStyle: 'smallitalics', 
customColor: 'highlight')
Note: If you copy and paste the Markdown above, make sure you remove the line breaks.

Markdown Preview app with custom attributes not applied on the rendered attributed string.

The attributed string looks like a normal string without any styles. But check the log, and you’ll see the following:

BoldCaps and Danger {
	customColor = danger
	customStyle = boldcaps
	NSPresentationIntent = [paragraph (id 1)]
SmallItalics and Highlighted {
	customStyle = smallitalics
	customColor = highlight
	NSPresentationIntent = [paragraph (id 1)]

The attributed string has the correct custom attributes, so they were decoded correctly. What’s missing?

At this point, the UI doesn’t know what to do with those attributes. The information from the attributed string is stored properly but is completely unknown to SwiftUI.Text, which is rendering the attributed string.

Rendering Custom Attributes

To properly render your custom attributes, you’ll need to create your own view to work with them. You might think you’ll need to draw the text yourself and take care of low-level rendering operations on the screen. You don’t need to worry about any of this! This class is a lot simpler than you might expect. All you need to do is transform the custom attributes to normal attributes that a standard Text view can understand, then use a Text view normally.

Create a new SwiftUI view in the Subviews group, and name it CustomText.swift. Replace the contents of the file with the following:

import SwiftUI

public struct CustomText: View {
  // 1
  private var attributedString: AttributedString

  // 2
  private var font: Font = .system(.body)

  // 3
  public var body: some View {

  // 4
  public init(_ attributedString: AttributedString) {
    self.attributedString = 
      CustomText.annotateCustomAttributes(from: attributedString)

  // 5
  public init(_ localizedKey: String.LocalizationValue) {
    attributedString = CustomText.annotateCustomAttributes(
      from: AttributedString(localized: localizedKey, 
        including: \.customAttributes))

  // 6
  public func font(_ font: Font) -> CustomText {
    var selfText = self
    selfText.font = font
    return selfText

  // 7
  private static func annotateCustomAttributes(from source: AttributedString) 
    -> AttributedString {
    var attrString = source

    return attrString

Going over the details of this new view — here, you:

  1. Store the attributed string that will appear.
  2. Store the font and set a default value with Font.system(body).
  3. Ensure the body of the view has a standard SwiftUI.Text to render the stored attributedString.
  4. Set an initializer similar to SwiftUI.Text to take an attributed string as a parameter. Then, call the private annotateCustomAttributes(from:) with this string.
  5. Give a similar initializer a localization key, then create an attributed string from the localization file.
  6. Add a method to create and return a copy of the view with a modified font.
  7. Do nothing meaningful in this method — at least for now. This is where the real work will be. Currently, all it does is copy the parameter in a variable and return it. You’ll implement this shortly.

Next, in Views/MarkdownView.swift, change the view type that’s showing the converted Markdown from Text to CustomText. The contents of HStack should be:

  .padding(.top, 4.0)

Build and run. Make sure that you didn’t break anything. Nothing should look different from before.

Return to Subviews/CustomText.swift and add the following before the return in annotateCustomAttributes(from:):

// 1
for run in attrString.runs {
  // 2
  guard run.customColor != nil || run.customStyle != nil else {
  // 3
  let range = run.range
  // 4
  if let value = run.customStyle {
    // 5
    if value == .boldcaps {
      let uppercased = attrString[range] {
      attrString.characters.replaceSubrange(range, with: uppercased)
      attrString[range].inlinePresentationIntent = .stronglyEmphasized
    // 6
    } else if value == .smallitalics {
      let lowercased = attrString[range] {
      attrString.characters.replaceSubrange(range, with: lowercased)
      attrString[range].inlinePresentationIntent = .emphasized
  // 7
  if let value = run.customColor {
    // 8
    if value == .danger {
      attrString[range].backgroundColor = .red
      attrString[range].underlineStyle =
        Text.LineStyle(pattern: .dash, color: .yellow)
    // 9
    } else if value == .highlight {
      attrString[range].backgroundColor = .yellow
      attrString[range].underlineStyle =
        Text.LineStyle(pattern: .dot, color: .red)

This might seem like a long block of code, but it’s actually quite simple. Here’s what it does:

  1. Loops on the available runs in the attributed string.
  2. Skips any runs that don’t have any value for customColor nor customStyle.
  3. Stores the range of the run for later use.
  4. Checks if the run has a value for customStyle.
  5. If that value is boldcaps, then creates a string from the characters in the range of the run and converts them to uppercase. Replace the text in the attributed string in the run’s range with the new uppercase characters, then applies the bold style stronglyEmphasized.
  6. Otherwise, if the value is smallitalics, then do the same as above, except using lowercase characters with italic style emphasized instead.
  7. Checks without an else if customColor has a value.
  8. If the value is danger, sets the background color to red and the underline style to a yellow dashed line.
  9. Otherwise, if the value is highlight, sets a yellow background and the underline style to a red dotted line.

Build and run. Try the same Markdown from the previous example.

The rendered Markdown using the new CustomText view

Now your custom attributes are visible. Try choosing different themes. As expected, your themes changed the style of the text. Switching the themes also works.

Your attributed string isn’t altered when it appears. Your custom view copies it, so it can change safely, separate from the original.

Saving Styled Strings

The final part of your app is building the strings library. The app should show a list of all the saved attributed strings and use the Markdown previewer to add new strings.

First, change the navigation flow of the app to open a list first instead of the previewer. Open assets from this tutorial’s materials, and drag SavedStringsView.swift onto the Views group. Make sure to check Copy items if needed.

Adding the new view file to the project

Then, go to MarkdownView.swift and add this new property at the top of the structure:

var dataSource: AttributedStringsDataSource<AttributedString>

In the preview code in the same file, change the creation of MarkdownView to:

MarkdownView(dataSource: AttributedStringsDataSource())

Finally, in AppMain.swift show SavedStringsView instead of MarkdownView:

struct AppMain: App {
  var body: some Scene {
    WindowGroup {
      SavedStringsView(dataSource: AttributedStringsDataSource())

Build and run. Your app now opens directly on the Saved Strings screen, and it has a + at the top-right corner to open the Markdown Preview screen.

Empty strings list with a plus at the top right corner

The listing screen passes the data source responsible for the persistence of the saved strings, but the preview screen doesn’t have any actions yet that allow you to save the attributed string you’re viewing.

To fix this, go to MarkdownView.swift and add the following to the structure, just above the definition of convertMarkdown(_:):

func saveEntry() {
  let originalAttributedString = convertMarkdown(markdownString)

func cancelEntry() {

Add the following near the end of body, after .navigationTitle("Markdown Preview"):

  leading: Button(action: cancelEntry) {
  trailing: Button(action: saveEntry) {

Build and run. Add some values with the custom attributes, perhaps by copying and pasting the same Markdown you used earlier, then restart the app.

The attributed string with custom attributes looks like plain text

Saving Custom Attributes

When you saved your string, the formatting showed in the list — but when you relaunch the app, its styling is lost! There’s a simple reason for this. When you saved the string the first time, it was directly added to the data source array. But when you relaunched the app, the data source reloaded everything from a file. The attributed string with all its custom attributes was saved correctly in the file, but the custom attributes aren’t there the second time you launch the app.

The decoder in the data source doesn’t know anything about the attributes scope you created to encode and decode your custom attributes. And in a way, it shouldn’t care about that since it’s a generic decoder.

Saving Fonts

There’s also another issue. If you created some strings with a theme, you’ll notice that the font is also lost in the decoding process. This time, it’s not something you missed. It seems that SwiftUI.Font doesn’t conform to Codable, so its value isn’t stored.

To fix the first issue, you’ll need to wrap an attributed string in another type and configure the attributed string property with a Codable configuration to consider the new scope. And for the second, you’ll just save the selected theme alongside the attributed string and reapply the theme when you add it to the list.

Create a new Swift file in the Models group named CustomAttributedString.swift. Add the following to it:

import SwiftUI

struct CustomAttributedString: Codable, Identifiable, Hashable {
  // 1
  func hash(into hasher: inout Hasher) {
  // 2
  var id: Int {
  // 3
  var textTheme: TextTheme
  // 4
  @CodableConfiguration(from: \.customAttributes) var attributedString = 
  // 5
  init(_ attString: AttributedString, theme: TextTheme) {
    attributedString = attString
    textTheme = theme
  // 6
  var themedString: AttributedString {
    var tempString = attributedString
    return tempString

Here’s what this does:

  1. Uses the hashing function to generate the hash of the current object by merging the hash values of its two stored properties.
  2. Defines a property, id, that returns the hash value. Identifiable requires this property.
  3. Sets a property for the theme of this string to reapply it whenever you need the string.
  4. Attaches a CodableConfiguration with the AttributeScopes.CustomAttributes type that you created to handle decoding the custom attributes.
  5. Adds an initializer for the new type.
  6. Sets a computed property for the attributed string that reapplies the theme.

Next, you’ll need to apply some changes to accommodate for the new type. In MarkdownView.swift, change the type of dataSource to:

var dataSource: AttributedStringsDataSource<CustomAttributedString>

Then, change the implementation of saveEntry() to:

func saveEntry() {
  let originalAttributedString = convertMarkdown(markdownString)
  let customAttributedString = CustomAttributedString(
    theme: selectedTheme)

This saves the new type that includes the original attributed string along with the theme instead of the attributed string alone.

Next, open SavedStringsView.swift and change the type of dataSource to:

@ObservedObject var dataSource: 

Finally, in the body, change the loop that adds the strings in the list to:

ForEach(dataSource.currentEntries, id: \.id) { item in

This will use the new type in the listing page and recreate the theme attributes in the attributed string for presentation.

Uninstall the app from the simulator by long-tapping the app icon. Then, tap Remove app to delete the previously saved list.

Uninstalling an app from the simulator

Build and run. Save some strings with different themes, then restart the app.

Attributed strings with different themes in the list.

You’ll see that all the styles are correctly applied in the list.

Where to Go From Here?

You can download the completed project files by clicking Download Materials at the top or bottom of the tutorial.

To learn more about AttributedString, have a look at the developer documentation.

You can also check out Apple’s session from WWDC21 that introduces AttributedString. The session references a sample project that illustrates overlapping attributes, localization and even rainbow text!

You should also check out SwiftUI Localization Tutorial for iOS: Getting Started to learn more about localization, pluralizations and grammar.

We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!