Using rich text in the TextEditor with SwiftUI

Using rich text in the TextEditor with SwiftUI

Explore the usage of rich text within the TextEditor in SwiftUI using AttributedString.

When you need to display and edit long-form text in SwiftUI, TextEditor is often the natural choice. By default, it works with a simple String and behaves as a plain text editor, making it ideal for simple notes or basic input fields.

But starting with iOS 26, macOS 26, and related platforms, TextEditor gained first-class support for AttributedString. With this change, you can transition from editing plain text to creating fully formatted rich text, complete with Markdown, links, attribute transformations, and more.

Plain Text Editing

The most basic use case of TextEditor involves binding it to a String.

struct PlainTextEditor: View {
    @State private var plainText = "This is plain text"

    var body: some View {
        TextEditor(text: $plainText)
    }
}

This editor is straightforward: it captures user input as unformatted text. If you only need to store or process raw strings, this is often sufficient.

Enabling Rich Text with AttributedString

To enable formatting, change the type of the variable bound to the text editor to AttributedString.

struct RichTextEditor: View {
    @State private var richText: AttributedString = "This is rich text"

    var body: some View {
        TextEditor(text: $richText)
    }
}

With this small change, the editor automatically supports styling. Users can add bold, italic and underline style directly inside the editor, and your app captures these attributes as part of the AttributedString.

Creating and combining Attributed Strings

AttributedString is not limited to editing. You can also build rich text programmatically by combining multiple instances.

Append:

var a = AttributedString("Hello, ")
var b = AttributedString("world")
b.font = .body.bold()
a.append(b)

Operator +=:

a += AttributedString("!")

Insert:

if let index = a.firstIndex(of: ",") {
    a.insert(AttributedString(" dear"), at: a.index(after: index))
}

These operations preserve attributes correctly, making it easy to construct styled text dynamically.

Creating Rich Content with Markdown

AttributedString has built-in Markdown support as well. This means you can easily create rich text from Markdown content, whether it comes from user input or a server response.

do {
    let thankYou = try AttributedString(
        markdown: "**Thank you!** Visit our [website](<https://www.createwithswift.com>)"
    )
    print(thankYou)
} catch {
    print("Markdown parsing failed: \(error.localizedDescription)")
}

Markdown parsing respects attributes like bold, italic, links, and even inline code formatting.

Selection in Rich Text

Working with selections allows you to inspect or transform only part of the text.

struct RichSelectTextEditor: View {
    @State private var text: AttributedString = "This is rich text"
    @State private var selection = AttributedTextSelection()

    var body: some View {
        TextEditor(text: $text, selection: $selection)
    }
}

With AttributedTextSelection, you gain access not only to the text ranges but also to the attributes applied within those ranges.

Inspecting and Modifying Attributes

Selections don’t just give you ranges of text. They also provide typingAttributes: the attributes that will be applied to newly inserted text at the current cursor position.

let typingAttributes = selection.typingAttributes(in: text)

With typing attributes, you can synchronize your UI with the editor state. For example, you might enable or disable a “Bold” toggle depending on whether the cursor is currently inside bold text.

If you want to apply formatting changes instead, use this method:

attributedText.transformAttributes(in: &selection) { attributes in
    ...
}

This method works in three important ways:

  1. &selection as inout parameter
    The selection is passed with & because SwiftUI may need to adjust it while attributes are applied. For example, if adjacent runs merge after editing, the selection stays valid.
  2. attributes as an inout AttributeContainer
    Inside the closure, you receive an AttributeContainer, which represents a dictionary of text attributes. Mutating it updates the attributes for the entire selection.
  3. Safe and consistent updates
    Unlike directly modifying individual ranges, transformAttributes ensures that attribute runs are merged when possible, keeping your attributed text normalized and avoiding unnecessary fragmentation.

There are many different attributes you can safely transform with this method:

attributedText.transformAttributes(in: &selection) { attributes in
    // Font styling
    attributes.font = (attributes.font ?? .default).bold(true)
    attributes.font = (attributes.font ?? .default).italic(true)
    attributes.font = .system(size: 18, weight: .semibold, design: .rounded)
    
    // Underline and strikethrough
    attributes.underlineStyle = .single
    attributes.strikethroughStyle = .single
    attributes.strikethroughColor = .red
    
    // Colors
    attributes.foregroundColor = .blue
    attributes.backgroundColor = .yellow
    
    // Links
    attributes.link = URL(string: "https://www.createwithswift.com")
    
    // Typography adjustments
    attributes.baselineOffset = 2
    attributes.kern = 1.5
}

By combining these attributes, you can build editing features that go well beyond bold and italic, everything from links to custom colors, typography adjustments and much more.

A Formatting Toolbar Example

Here’s how to connect a simple toggle to the editor’s current selection:

@Environment(\.fontResolutionContext) private var fontResolutionContext


Toggle(
    "Toggle Bold",
    systemImage: "bold",
    isOn: Binding(
        get: {
            let font = attributedTextSelection.typingAttributes(in: attributedText).font
            let resolved = (font ?? .default).resolve(
                in: fontResolutionContext
            )
            return resolved.isBold
        },
        set: { isBold in
            attributedText.transformAttributes(in: &attributedTextSelection) {
                $0.font = ($0.font ?? .default).bold(isBold)
            }
        }
    )
)

This pattern works for bold, italic, underline, color, and other attributes. With a few controls, you can build a functional text formatting toolbar.

The fontResolutionContext environment value helps you resolve the typingAttributes based on the user context. Apple made it an environment value because fonts in SwiftUI are adaptive resources, not absolute values. By resolving fonts in the environment, you ensure that formatting logic in your editor always matches what the user actually sees, including dynamic type, accessibility, and platform-specific typography.


Switching from String to AttributedString in TextEditor opens up a complete set of rich text capabilities in SwiftUI. With built-in Markdown parsing, AttributedTextSelection, attribute transformations, and composition methods such as append and insert, you can create editing experiences that go far beyond plain text.

These tools give you the foundation to implement anything from lightweight note editors to feature-rich text fields that rival dedicated word processors, all natively in SwiftUI.