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

4 (1) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Runs

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
  print(run)

  // 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 = UIColor.blue
  case .important:
    container.font = .custom("Courier New", size: 17, relativeTo: .body)
    container.backgroundColor = .yellow
  default:
    break
  }
  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:

attributedString.mergeAttributes(selectedTheme.attributeContainer)

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.
  • 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 {
    self[T.self]
  }
}

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.