Choosing a Text Editor in SwiftUI: Why I Moved to RichTextKit
Balancing performance, compatibility, and flexibility when editing text in SwiftUI apps
After spending quite a bit of time working with SwiftUI's built-in TextField and TextEditor, I got more and more frustrated.
Both controls have their own benefits but each control had pain points that eventually led me to adopting a third-party library - RichTextKit - as the text editor for my app.
I'll walk through the rationale and some of the lessons learned as I go.
Why TextField Didn’t Work for Me
Initially, I relied heavily on TextField. It was surprisingly capable control. You can use .axis(.vertical) to support multiple lines of editing, and the built-in selection property allowed users to select text easily to edit part of the text.
However, performance became an issue. As soon as the text got big enough, the control performance would start to get worse. I still do not know the cause, but the lag was so bad that I had to walk away.
Switching to TextEditor… and running into a wall
Next, I looked to TextEditor. Performance was noticeably better than TextField - it was much smoother, even with larger text, and it had selection so seemed like a reasonable alternative.
However, as I dug deeper, I ran into the key issue - a selection bug. If you delete text until the input field is empty, and then typed again, it could throw an out-of-bounds error. I searched widely and tried all the workarounds I could find, but I was not able to overcome it.
The bug would still persist on macOS 15.5. I have not yet worked with macOS 26 (I still need to support earlier versions of the OS), but that limitation was enough for me to hit a wall.
Why I chose to try RichTextKit
That’s where I found RichTextKit on GitHub. While macOS 26 has purported improvements in performance and provides more attributes to TextEditor, I cannot depend on that for the people still using earlier versions. RichTextKit filled that void.
What's nice about it:
It connects UITextView (iOS) and NSTextView (macOS).
It has good selection support.
It uses NSAttributedString, so I can create and apply local formatting like colors, font changes, or comments.
It performs reasonable compared to native options.
Here is the basic setup:
struct MyView: View {
@State
private var text = NSAttributedString(string: "Type here...")
@StateObject
var context = RichTextContext()
var body: some View {
RichTextEditor(text: $text, context: context) {
// Customize the native text view here
}
.focusedValue(\.richTextContext, context)
}
}
text: NSAttributedString: The attributed string you control and observe. You can change font, color, or other hues by applying attributes to parts of your range.
context: RichTextContext: A controller to manipulate text programmatically that is familiar to UIKit/AppKit users. It is powerful and adaptable.
Lessons learned and little nuggets
1. Color handling for the background with colorScheme
RichTextKit makes available a default background color. That makes sense if you accept it, but my app has it's own theme, and I wanted to do some custom styling.
The deal: you can't just pass colorScheme through as a property inside viewConfiguration. It's not going to work. You need to escape the closure:
if colorScheme == .dark {
RichTextEditor(text: $text, context: context, viewConfiguration: {
if let textView = $0 as? NSTextView {
textView.backgroundColor = NSColor.black
}
})
} else {
RichTextEditor(text: $text, context: context, viewConfiguration: {
if let textView = $0 as? NSTextView {
textView.backgroundColor = NSColor.white
}
})
}
This method worked right in my testing. I would be glad to learn if there is a more elegant way to do it.
2. Converting from NSAttributedString to String
RichTextKit uses NSAttributedString, so you will need a form of conversion when saving data, making a network request, or displaying plain text inside SwiftUI's Text.
To String:
let attributed = NSAttributedString(string: "Hello")
let plain = attributed.string
Back to NSAttributedString
:
let plain = "Hello Swift"
let attributed = NSAttributedString(string: plain)
3. To update fonts or colors
With attributed strings, the attributes are the foundation, as opposed to .font or .foregroundStyle in SwiftUI.
let attributes: [NSAttributedString.Key: Any] = [
.font: NSFont(name: "Times New Roman", size: 18)!,
.foregroundColor: NSColor.textColor
]
let text = NSAttributedString(string: string, attributes: attributes)
To update anything programmatically, you would call:
context.setAttributedString(to: text)
4. Working with selection
One of the best features for RichTextKit is the control of selection.
context.hasSelectedRange will tell you if the text is highlighted.
context.resetSelectedRange() will clear the selection.
context.selectedRange will return an NSRange.
You can use SwiftUI's .onChange to listen to selection and do things with the highlighted text:
.onChange(of: context.selectedRange) {
let range = context.selectedRange
let result = splitByRange(in: text, range: range)
let selection = result.selected
}
Here is a helper to split the string:
func splitByRange(in content: String, range: NSRange) -> (prefix: String, selected: String, suffix: String) {
if content.isEmpty || range.location > content.utf16.count || range.length == 0 {
return (content, "", "")
}
guard let swiftRange = Range(range, in: content) else {
return (content, "", "")
}
let prefix = String(content[..<swiftRange.lowerBound])
let selected = String(content[swiftRange])
let suffix = String(content[swiftRange.upperBound...])
return (prefix, selected, suffix)
}
This lets me extract exactly what’s before, inside, and after the selection — something my app depends on.
5. Built-in toolbars
Finally, RichTextKit has toolbars that save you a lot of work:
VStack(spacing: 0) {
#if os(macOS)
RichTextFormat.Toolbar(context: context)
#endif
RichTextEditor(text: $document.text, context: context) {
$0.textContentInset = CGSize(width: 30, height: 30)
}
#if os(iOS)
RichTextKeyboardToolbar(
context: context,
leadingButtons: { $0 },
trailingButtons: { $0 },
formatSheet: { $0 }
)
#endif
}
The built-in formatting and keyboard toolbars are so feature-complete that I didn’t have to clone them.
Final Thoughts
Apple is improving in ios/macOS 26, and rapidly adding new attributes and optimizations, but right now many of us need to support the older OS versions the way things are, RichTextKit is a solid performant option.
It's not perfect, and I'm still figuring out its idiosyncrasies, but it has to be better than the built-in controls, which have at least provided me significantly more stability, flexibility, and control. If you are building a SwiftUI app today and want to support more than just the last OS, I think it is worth looking into.