Apple Text View Picker
Authored against iOS 26.x / Swift 6.x / Xcode 26.x.
This skill is a capability comparison across the Apple text views. The capability matrix below is a starting point; before committing a view choice, open the actual feature requirements list and confirm each line item against the matrix — view selection mistakes propagate into wrapper code, performance work, and Writing Tools integration that are expensive to undo. If you are deciding based on a single requirement, you have not surveyed the full requirement set yet.
A view class isn't picked from the framework alone. SwiftUI Text displays attributed strings but ignores paragraphStyle. SwiftUI TextField accepts a vertical axis since iOS 16 and handles many cases that previously required UITextView. SwiftUI TextEditor gained real rich-text editing on iOS 26 but still cannot do inline images, lists, or TextKit access. The right answer depends on the combination of features you need, the deployment floor, and whether the view will be wrapped, embedded, or composed.
Contents
- The view classes
- Display vs editing
- SwiftUI plain-text editing
- When you need TextKit access
- Common decisions
- Quick code patterns
- Common mistakes
- References
The view classes
Five views cover almost every case:
Text(SwiftUI) — read-only display. RendersString,LocalizedStringKey, and a defined subset ofAttributedStringattributes. No editing, no cursor.TextField(SwiftUI) — single-line editing by default; multi-line viaaxis: .verticalsince iOS 16. PlainStringbinding, withformat:for typed values.TextEditor(SwiftUI) — multi-line editing. PlainStringon iOS 14-25;AttributedStringrich-text editing on iOS 26+.UITextView(UIKit) — full TextKit-backed editor. Attributed text, attachments, layout managers, custom rendering, Writing Tools.NSTextView(AppKit) — desktop counterpart with field-editor architecture, text tables, rulers, Services menu, NSText heritage (RTF/RTFD I/O).
UILabel / NSTextField exist for the cases where a view doesn't wrap into the SwiftUI hierarchy. They're rarely the answer in a SwiftUI app — Text and TextField cover the same ground.
Display vs editing
If the text is read-only, the choice collapses fast. SwiftUI Text is the right answer for nearly all read-only display, including styled AttributedString, inline Markdown literals, and dynamic text composition via the + operator. The exceptions are narrow:
- Need range-select-and-copy on iOS —
Text.textSelection(.enabled)only supports select-all on iOS; range selection works on macOS but not iOS. - Need TextKit-rendered features the SwiftUI subset omits (paragraph styles, attachments, exclusion paths, custom rendering attributes) — drop to a
UITextVieworNSTextViewconfigured non-editable. - Need to render block-level Markdown (headings, lists, blockquotes) —
Textonly renders inline Markdown; block structure ends up inpresentationIntentand is silently ignored. Either preprocess into a SwiftUI view tree or render via TextKit.
For editing, the question is what kind of input.
SwiftUI plain-text editing
If the binding can stay as String, SwiftUI is usually the right call. TextField covers single-line and (since iOS 16) modest multi-line growth. TextEditor covers always-multi-line editing.
TextField(axis: .vertical) is underused — it grows to fit content, accepts lineLimit(2...8) for bounded growth, supports a placeholder via the prompt parameter, and behaves correctly inside forms and lists. For chat composers and comment fields it is almost always the right answer over TextEditor or a wrapped UITextView.
TextEditor is the right choice when the editor must always be multi-line, when there is no placeholder requirement, or when you need iOS 26 rich-text editing. On iOS 25 and earlier, TextEditor is plain-text only and has no prompt — overlay a Text view manually if a placeholder is needed, or use TextField(axis: .vertical).
The iOS 26 rich-text variant (TextEditor with AttributedString binding) handles bold/italic/underline, foreground/background colors, alignment, line height, and writing direction. Genmoji insertion works. What it can't do — inline images, lists, tables, exclusion paths, TextKit access, custom layout — is a real ceiling, so check requirements before picking it for anything beyond simple rich text.
When you need TextKit access
The boundary at which SwiftUI stops working is well-defined. Drop to UITextView (or NSTextView) when any of these are true:
- You need to inspect or manipulate
textStorage,layoutManager(TK1), ortextLayoutManager(TK2). - You need temporary attributes for syntax highlighting (TextKit 1) or rendering attributes (TextKit 2).
- You need inline
NSTextAttachmentviews. - You need exclusion paths, multi-column layout, or
NSTextTable. - You need full Writing Tools delegate control beyond the default behavior.
- You need a custom input accessory view, custom keyboard, or marked-text handling.
- You need spellcheck customization beyond
UITextInputTraits.
Once any of these is on the requirement list, SwiftUI text views become a poor fit and a UIViewRepresentable wrapping UITextView (or NSViewRepresentable wrapping NSTextView inside an NSScrollView) is the path. The wrapping mechanics are non-trivial — that is its own skill.
UITextView and NSTextView are not interchangeable. UITextView is a UIScrollView, has UITextInteraction for modular gestures, and gained UITextItem interactions in iOS 17. NSTextView lives inside an NSScrollView, owns text tables, ruler, font panel, Services menu, and NSText's RTF I/O. Cross-platform code typically wraps each in its own representable.
Common decisions
A few cases come up often enough to spell out:
Chat composer that grows vertically.
TextField(axis: .vertical)first. Drop to a wrappedUITextViewonly if you need attributed editing, attachments, or TextKit features. Don't reach forTextEditorhere — it always takes its full proposed height and has no placeholder.Notes editor with rich text on iOS 26+. Try
TextEditorwithAttributedStringfirst. Drop to a wrappedUITextViewif you need attachments, lists, or TextKit access.Notes editor with rich text on iOS 25 or earlier. Wrapped
UITextView. PlainTextEditordoesn't acceptAttributedStringon those versions.Syntax-highlighted code editor. Wrapped
UITextView/NSTextView. TextKit 1 if you need temporary attributes (proven, fast) or glyph metrics; TextKit 2 if viewport performance on huge files is critical. Neither is "legacy" or "modern" — they solve different problems.Static styled label.
Textwith anAttributedStringor inline Markdown literal.UILabelonly when you can't be in SwiftUI.Settings-style form input.
TextField. Useformat:for typed values (currency, integers, dates).Markdown rendering, display only. If only inline Markdown (bold, italic, links, code) —
TextwithAttributedString(markdown:). If block-level (headings, lists, quotes) — TextKit-backed view or a third-party SwiftUI Markdown renderer.Document editor with text tables, rulers, or printing.
NSTextViewon macOS. UIKit has no equivalent for text tables or rulers.
Quick code patterns
Read-only styled text in SwiftUI:
// Inline Markdown literal — interpreted at compile time
Text("Visit **[example.com](https://example.com)** today.")
// Runtime AttributedString
var attr = AttributedString("Important note")
attr.foregroundColor = .red
attr.font = .body.bold()
Text(attr).textSelection(.enabled)
Single-line input with a typed value:
@State private var price: Double = 0
TextField("Price", value: $price, format: .currency(code: "USD"))
.textFieldStyle(.roundedBorder)
.keyboardType(.decimalPad)
.submitLabel(.done)
Vertical-axis chat composer:
TextField("Compose…", text: $body, axis: .vertical)
.lineLimit(2...8)
iOS 26 rich-text editing:
@State private var text = AttributedString("Edit this text")
var body: some View {
TextEditor(text: $text)
}
Wrapped UITextView for rich editing on older iOS or when TextKit access is needed:
struct RichTextEditor: UIViewRepresentable {
@Binding var attributedText: NSAttributedString
func makeUIView(context: Context) -> UITextView {
let tv = UITextView()
tv.delegate = context.coordinator
tv.backgroundColor = .clear // let SwiftUI background show
return tv
}
func updateUIView(_ tv: UITextView, context: Context) {
guard tv.attributedText != attributedText else { return } // prevent loop
tv.attributedText = attributedText
}
func makeCoordinator() -> Coordinator { Coordinator(self) }
final class Coordinator: NSObject, UITextViewDelegate {
var parent: RichTextEditor
init(_ p: RichTextEditor) { parent = p }
func textViewDidChange(_ tv: UITextView) {
parent.attributedText = tv.attributedText
}
}
}
The wrapper above is a starting sketch. Real-world wrappers have to handle focus, sizing, cursor preservation, and update-loop guards correctly — see the wrap-textview skill.
Common mistakes
Reaching for
UITextViewwhenTextField(axis: .vertical)would work. The vertical-axisTextFieldcovers the chat-composer case natively since iOS 16, including placeholder, line-limit growth, and form integration. A wrappedUITextViewis the right answer only when attributed editing, attachments, or TextKit access is on the requirement list. Defaulting to a wrapper to "be safe" buys complexity you'll pay for in update-loop bugs and focus management.Expecting full Markdown to render in SwiftUI
Text. Inline Markdown (bold, italic, code, links) renders. Block-level Markdown (headings, lists, blockquotes, code blocks) is parsed intopresentationIntentand silently dropped from the rendered output. The text appears unformatted. If block-level rendering matters, use TextKit, a third-party SwiftUI Markdown view, or render the parsed structure into a SwiftUI view tree manually.Setting
attributedTextinupdateUIViewwithout an equality check. Each set triggerstextViewDidChange, which writes back to the binding, which callsupdateUIView, which setsattributedTextagain. Infinite loop, or at minimum cursor jumps every keystroke. Guard withguard tv.attributedText != attributedText else { return }.Relying on
Text.textSelection(.enabled)for range selection on iOS. On iOS, the modifier enables select-all only. Range selection works on macOS but not iOS. If users need to copy a substring on iOS, use a non-editableUITextViewinstead.Wrapping
UITextViewwithoutbackgroundColor = .clear. UIKit's defaultsystemBackgroundcolor paints over any SwiftUI background, list separator styling, or material effect behind the wrapped view. Always clear the background and let SwiftUI's chrome show through.Using
TextEditorwhen you actually need a placeholder.TextEditorhas nopromptparameter on any iOS version. Either overlay aTextview manually (showing/hiding based on the binding being empty) or switch toTextField(axis: .vertical), which haspromptand similar growth behavior.Picking
TextEditorwithAttributedStringfor production rich-text on iOS 26. It works for simple cases. It cannot do inline images, lists, tables, exclusion paths, or TextKit access. If rich text is mission-critical or the app needs iOS 25 support, a wrappedUITextViewremains the safer choice. Treat the iOS 26 path as additive, not a replacement.
References
references/reference.md— capability matrix and platform-by-platform reference, loaded only when neededreferences/examples.md— usage-oriented examples, loaded only when needed/skill txt-wrap-textview— wrappingUITextView/NSTextViewin SwiftUI/skill txt-swiftui-interop— which AttributedString attributes survive the SwiftUI/TextKit boundary/skill txt-swiftui-texteditor— iOS 26 SwiftUI TextEditor rich-text APIs/skill txt-textkit-choice— TextKit 1 vs TextKit 2 decision/skill txt-appkit-vs-uikit— NSTextView vs UITextView capability comparison- SwiftUI Text
- SwiftUI TextField
- SwiftUI TextEditor
- UITextView
- NSTextView