5 min read20 hours ago
–
SwiftUI Rich Text Editing with TextEditor and AttributedString
Press enter or click to view image in full size
SwiftUI’s TextEditor
combined with AttributedString
provides a powerful way to create rich text editing experiences directly in your iOS, macOS, and other Apple platform apps. With the recent enhancements in SwiftUI, you can now build sophisticated text editors without dropping down to UIKit or AppKit.
Introduction
If you’ve ever tried building a text editor in SwiftUI before iOS 26, you know the pain.
You’d get about 80% of the way there with TextEditor
, and then — bam — you’d hit a wall the moment you needed bold text, colors, or inline styles. The solution? Usually dropping into UITextView
, juggling attributed strings, and wrest…
5 min read20 hours ago
–
SwiftUI Rich Text Editing with TextEditor and AttributedString
Press enter or click to view image in full size
SwiftUI’s TextEditor
combined with AttributedString
provides a powerful way to create rich text editing experiences directly in your iOS, macOS, and other Apple platform apps. With the recent enhancements in SwiftUI, you can now build sophisticated text editors without dropping down to UIKit or AppKit.
Introduction
If you’ve ever tried building a text editor in SwiftUI before iOS 26, you know the pain.
You’d get about 80% of the way there with TextEditor
, and then — bam — you’d hit a wall the moment you needed bold text, colors, or inline styles. The solution? Usually dropping into UITextView
, juggling attributed strings, and wrestling with UIKit delegates.
But not anymore.
With iOS 26, SwiftUI finally gives us real rich text editing — powered by AttributedString
, AttributedTextSelection
, and FontResolutionContext
.
That means you can now build a text editor that supports bold, italic, colors, underline, and even dynamic font sizes, all with clean, native SwiftUI code.
No UIKit bridges. No hacks. Just SwiftUI the way it was meant to be.
The Core Ideas
Before diving into the code, let’s quickly cover the key pieces that make this possible — and why they matter.
**AttributedString**
is your new best friend.
It’s a Swift-native, value-type string that can hold rich formatting information — things like font, color, underline, background, etc. Think of it as NSAttributedString
, but actually pleasant to work with.
**AttributedTextSelection**
tracks what part of the text the user has selected or where the cursor currently is. Instead of giving you raw ranges (which can easily go out of sync), SwiftUI wraps this up neatly, so you can safely apply or read formatting.
**FontResolutionContext**
is an environment value that knows how fonts resolve traits like bold or italic. Without this, toggling styles could get inconsistent, especially with custom fonts or accessibility scaling.
And finally, **transformAttributes(in:)**
— this is the magic function. It lets you modify attributes on just the selected text, safely and efficiently.
Together, these APIs make up the new building blocks for modern text editing in SwiftUI.
Setting Up a Simple Editor
Let’s start with the most basic setup — a place where we can edit and select text.
struct ContentView: View { @Environment(\.fontResolutionContext) var fontResolutionContext @State private var attributedText = AttributedString("Start typing here...") @State private var selection = AttributedTextSelection() var body: some View { TextEditor(text: $attributedText, selection: $selection) .padding() }}
This gives you a working text editor backed by an AttributedString
.
The @Environment
part (fontResolutionContext
) is what lets SwiftUI handle traits like bold and italic correctly — it’s subtle but essential.
Making Text Bold and Italic
Now for the fun part: applying formatting.
This is where transformAttributes
really shines. You can think of it as your text editor’s “formatting function” — it modifies the attributes for whatever text is selected.
func toggleBold() { attributedText.transformAttributes(in: &selection) { container in let font = container.font ?? .body let resolved = font.resolve(in: fontResolutionContext) container.font = font.bold(!resolved.isBold) }}func toggleItalic() { attributedText.transformAttributes(in: &selection) { container in let font = container.font ?? .body let resolved = font.resolve(in: fontResolutionContext) container.font = font.italic(!resolved.isItalic) }}
Each toggle reads the current font, checks if it’s bold or italic, and flips that state — only for the selected range. No manual range management. No font string parsing. Just clean, declarative Swift.
Reflecting Current Styles in the UI
You’ll probably want to highlight your toolbar buttons when the text is bold or italic — just like in Pages or Notes. Here’s how to check the active style at the current cursor position:
private var isBold: Bool { let attributes = selection.typingAttributes(in: attributedText) if let font = attributes.font { let resolved = font.resolve(in: fontResolutionContext) return resolved.isBold } return false}
SwiftUI automatically updates this when the user changes the selection, so your toolbar always stays in sync. No extra state juggling required.
Adding Underline and Strikethrough
Underline and strikethrough are even simpler — they’re just toggles on the attribute container.
func toggleUnderline() { attributedText.transformAttributes(in: &selection) { container in container.underlineStyle = container.underlineStyle == nil ? .single : nil }}func toggleStrikethrough() { attributedText.transformAttributes(in: &selection) { container in container.strikethroughStyle = container.strikethroughStyle == nil ? .single : nil }}
The API reads like English, which is one of the reasons I love this new system — it feels like SwiftUI was finally designed to handle text seriously.
Working with Colors
Adding color support is straightforward.
Here’s how you can apply text color and hook it up to a ColorPicker
:
func applyForegroundColor(_ color: Color) { attributedText.transformAttributes(in: &selection) { container in container.foregroundColor = color }}ColorPicker("Text Color", selection: $selectedColor) .onChange(of: selectedColor) { _, newColor in applyForegroundColor(newColor) }
You can do the same for background color by modifying container.backgroundColor
.
Building a Simple Toolbar
Now let’s put all these actions into a clean toolbar that reacts to the current formatting.
HStack(spacing: 12) { Button(action: toggleBold) { Image(systemName: "bold") .frame(width: 36, height: 36) .background(isBold ? Color.blue.opacity(0.2) : .clear) .cornerRadius(8) } Button(action: toggleItalic) { Image(systemName: "italic") .frame(width: 36, height: 36) .background(isItalic ? Color.blue.opacity(0.2) : .clear) .cornerRadius(8) } Button(action: toggleUnderline) { Image(systemName: "underline") .frame(width: 36, height: 36) .background(hasUnderline ? Color.blue.opacity(0.2) : .clear) .cornerRadius(8) }}.padding().background(Color(.systemGray6))
It’s simple, clear, and works beautifully across iPhone, iPad, and macOS.
Adjusting Font Size
Font resizing works with the same pattern — no special APIs needed.
func increaseFontSize() { fontSize = min(fontSize + 2, 72) attributedText.transformAttributes(in: &selection) { $0.font = .system(size: fontSize) }}func decreaseFontSize() { fontSize = max(fontSize - 2, 8) attributedText.transformAttributes(in: &selection) { $0.font = .system(size: fontSize) }}
This is super handy if you plan to add keyboard shortcuts or slider-based text controls later.
Clearing Formatting
Sometimes users want to “reset” their text. That’s easy too:
func clearFormatting() { attributedText.transformAttributes(in: &selection) { container in container.font = .body container.foregroundColor = .primary container.backgroundColor = nil container.underlineStyle = nil container.strikethroughStyle = nil }}
It’s one of those nice little touches that make your editor feel professional.
Performance and Saving
One of the coolest parts about these APIs is that they’re incredibly efficient.
AttributedString
is a value type, but it’s smart — it uses copy-on-write, so only the parts you actually edit get updated. Even large documents perform smoothly.
And since AttributedString
conforms to Codable
, saving and restoring is trivial:
// Saveif let data = try? JSONEncoder().encode(attributedText) { UserDefaults.standard.set(data, forKey: "document")}// Loadif let data = UserDefaults.standard.data(forKey: "document"), let decoded = try? JSONDecoder().decode(AttributedString.self, from: data) { attributedText = decoded}
This works great for drafts, local documents, or syncing with iCloud later.
The complete project is hosted on GitHub hare is the link to repository RichTextEditorDemo
Conclusion
These new text APIs in iOS 26+ finally give SwiftUI developers the tools we’ve been waiting for.
AttributedString
, AttributedTextSelection
, and transformAttributes
make it possible to build editors that are beautiful, native, and performant, without leaving the SwiftUI world.
Here’s what to remember:
- Use
transformAttributes(in:)
to apply formatting safely. - Always check traits through
fontResolutionContext
. - Use
typingAttributes(in:)
to keep your UI in sync. - Let SwiftUI handle the rendering — it’s fast enough for real-world use.
After years of bridging UIKit to make things work, this new API feels refreshingly elegant.
It’s everything we wanted
TextEditor
to be from the start — and it’s finally here.