TextEditor looks wonderfully simple:
TextEditor(text: $note)
People can type multiple lines, move the caret, long press, select text, and copy or paste. For an ordinary notes field, that is excellent.
The trouble begins when the app itself needs to understand that selection.
What if a Bold toolbar action should activate only when text is selected? What if a template must be inserted at the caret? Or if suggestions depend on the word the user has highlighted?
At that point, SwiftUI's TextEditor can feel more difficult than long-established UIKit controls.
Selection UI is not selection state
First, an important distinction: users can select text directly in TextEditor. iOS supplies the familiar caret, selection handles, and system Copy/Paste menu.
The historical gap was accessing the state of that selection from SwiftUI. The basic TextEditor(text:) API binds only the editor's contents to a String:
@State private var note = "Select part of this text."
TextEditor(text: $note)
We know the value of note, but not whether the user is at character 5 or has selected characters 5 through 20. That small distinction matters enormously to a feature-rich editor.
The good news: TextSelection in iOS 18
SwiftUI now has an official answer: TextSelection and the TextEditor(text:selection:) initializer in iOS 18.
import SwiftUI
struct NoteEditor: View {
@State private var text = "Select part of this text."
@State private var selection: TextSelection?
var body: some View {
TextEditor(text: $text, selection: $selection)
.frame(minHeight: 180)
}
}
TextSelection can represent an insertion point (the caret) or a range of text. An app can therefore respond to a user's selection without inspecting the UIKit view behind SwiftUI.
This is meaningful progress, but it has two practical consequences:
- The API requires iOS 18 or later.
- It does not remove the need for UIKit when an editor is highly customized or must support older iOS versions.
Why UIKit feels more direct
UIKit has had a mature text-input model for a long time through UITextInput. Both UITextField and UITextView expose selectedTextRange; for multiline editing, UITextView also exposes selectedRange as an NSRange.
Here is how to select all text in a UITextField:
let field = UITextField()
field.text = "INV-2026-0817"
if let range = field.textRange(
from: field.beginningOfDocument,
to: field.endOfDocument
) {
field.selectedTextRange = range
}
In UIKit, selection has long been part of the public API surface. You can read the active range, change it, and receive notifications when it changes.
One important note: the correct UIKit counterpart to a multiline TextEditor is UITextView, not UITextField. UITextField is still a useful example because both controls use the same UITextInput model.
| Need | SwiftUI TextEditor
|
UIKit |
|---|---|---|
| User selection with native iOS UI | Yes | Yes |
| Bind the text value | Binding<String> |
text plus target/delegate |
| Read or change selection in code |
TextSelection on iOS 18+ |
selectedTextRange / selectedRange
|
| Multiline editing | Yes | UITextView |
| Support selection state on older iOS | Needs another strategy | Long-standing API support |
| Highly customized input behavior | More limited | Broader delegate and UITextInput control |
The fallback: wrap UITextView
If your deployment target includes iOS 17 or earlier and selection is a product requirement, the straightforward fallback is a UITextView in UIViewRepresentable.
struct UIKitEditor: UIViewRepresentable {
@Binding var text: String
@Binding var selectedRange: NSRange
func makeUIView(context: Context) -> UITextView {
let view = UITextView()
view.delegate = context.coordinator
return view
}
func updateUIView(_ view: UITextView, context: Context) {
if view.text != text { view.text = text }
if view.selectedRange != selectedRange {
view.selectedRange = selectedRange
}
}
func makeCoordinator() -> Coordinator { Coordinator(self) }
final class Coordinator: NSObject, UITextViewDelegate {
var parent: UIKitEditor
init(_ parent: UIKitEditor) { self.parent = parent }
func textViewDidChange(_ textView: UITextView) {
parent.text = textView.text
}
func textViewDidChangeSelection(_ textView: UITextView) {
parent.selectedRange = textView.selectedRange
}
}
}
The wrapper delivers the required control, but it comes with cost: two-way synchronization, focus management, accessibility, Dynamic Type, and range validation whenever text changes.
One detail is easy to miss: NSRange uses UTF-16 offsets, not Swift Character counts. Emoji and combined characters can make index conversion a source of bugs. Do not assume that NSRange(location: 2, length: 1) simply means “the third character.”
A gap that is still noticeable after SwiftUI's 2019 debut
Apple introduced SwiftUI in 2019 and has clearly continued to invest in it. TextSelection in iOS 18 shows that Apple recognizes modern editor requirements.
Still, the gap is not entirely closed:
- The modern selection-state API arrived only in iOS 18, so it does not solve the problem for apps that support older systems.
- Developers of feature-rich editors still often need UIKit's deeper capabilities.
- Bridging to
UITextViewincreases the size and complexity of a codebase for behavior that has long been fundamental to UIKit text input.
The fair criticism is not “SwiftUI cannot select text.” Users can select text in TextEditor. The more precise criticism is this: observing and controlling selection from SwiftUI arrived late, and it still does not fully replace UIKit's depth.
Practical recommendation
- Use
TextEditor(text:)for a simple note editor. - Use
TextEditor(text:selection:)when iOS 18 is your minimum target and the app needs selection state. - Use a
UITextViewwrapper when you must support older iOS versions or need a highly customized editor. - Do not use
UITextFieldfor multiline content; useUITextView.
SwiftUI makes many things more elegant. For advanced text editing, however, UIKit remains an important escape hatch—which feels a little ironic after years of Apple investment in SwiftUI.
Top comments (0)