How to use TextEditor in SwiftUI | SwiftUI Bootcamp #36
TextField handles a single line. When you need a notes field, a bio, a message composer, or any multi-line text input, you reach for TextEditor. This lesson covers the key differences between the two, how to style TextEditor (including a workaround for its non-standard background behavior), and how to wire up a save action.
What You'll Learn
- What
TextEditoris, when to use it overTextField, and how it handles multi-line scrollable text - How
.colorMultiplyworks as a workaround for styling theTextEditorbackground - How to save text on demand and display the saved content below the editor
Mental Model
Think of TextField as a Post-it note — one line, limited space, you write it and stick it somewhere. TextEditor is a legal pad — multiple lines, scrollable, room to write paragraphs. In SwiftUI, TextEditor is the analog of UITextView from UIKit — it's a full-featured, scrollable text editing surface.
The main thing that trips people up with TextEditor is that it has a white background by default that you can't change with .background(Color.red) the way you can with other views. It ignores that modifier internally. The workaround is .colorMultiply(color), which applies a color multiplication blend to whatever is rendered — effectively tinting the background.
Detailed Explanation
TextEditor(text: $textEditorText) creates a multi-line, scrollable text input. Like TextField, it takes a binding to a String. Unlike TextField, there is no placeholder parameter — if you need a placeholder for TextEditor, you'll need to implement it yourself (usually by overlaying greyed-out Text that disappears when the binding is non-empty).
TextEditor expands to fill available space. Use .frame(height:) to constrain it to a fixed height, or omit the frame and let it fill the VStack. In this lesson, .frame(height: 250) gives it a defined area above the save button.
The background styling challenge: .background(Color.red) has no visible effect on TextEditor because the editor's internal UITextView draws its own white background on top of the SwiftUI background. .colorMultiply(Color(...)) works around this by applying a color-blend multiplication at the rendering level. Multiplying by a gray value darkens the white background to that gray. Multiplying by .white is the identity operation (no change). This isn't a perfect API, but it's the available workaround for iOS 14 and earlier.
In iOS 16+, .scrollContentBackground(.hidden) and .background(Color.myColor) work correctly together on TextEditor, making .colorMultiply unnecessary on modern targets.
Code Structure
TextEditorBootcamp.swift shows a TextEditor with a fixed height and a gray tinted background. A "Save" button copies textEditorText into savedText. The savedText string is displayed below the button, demonstrating the read-only display of saved content separately from the live editing area.
Complete Code
TextEditorBootcamp.swift
import SwiftUI
struct TextEditorBootcamp: View {
@State var textEditorText: String = "This is the starting text." // pre-populated with example content
@State var savedText: String = "" // holds the last saved version; separate from the live editing state
var body: some View {
NavigationView {
VStack {
TextEditor(text: $textEditorText) // multi-line scrollable editor, bound to textEditorText
.frame(height: 250) // constrain height; without this it expands to fill all space
//.foregroundColor(.red) // text color — works normally on TextEditor
//.background(Color.red) // does NOT work — TextEditor ignores this modifier
.colorMultiply(Color(#colorLiteral(red: 0.8374180198, green: 0.8374378085, blue: 0.8374271393, alpha: 1))) // tints the white background to light gray via blend multiplication
.cornerRadius(10)
Button(action: {
savedText = textEditorText // captures the current editor content as a snapshot
}, label: {
Text("Save".uppercased())
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(Color.blue)
.cornerRadius(10)
})
Text(savedText) // displays the saved snapshot — doesn't update live, only on Save tap
Spacer()
}
.padding()
//.background(Color.green)
.navigationTitle("TextEditor Bootcamp!")
}
}
}
struct TextEditorBootcamp_Previews: PreviewProvider {
static var previews: some View {
TextEditorBootcamp()
}
}Code Walkthrough
@State var textEditorText: String = "This is the starting text."— Pre-populated text shows in the editor on first render. This is useful for edit screens where you load existing content for the user to modify.@State var savedText: String = ""— A separate state for the committed/saved content. The editor content and saved content are intentionally separate — the user edits freely in the editor, and only tapping Save commits the change.TextEditor(text: $textEditorText)— Identical binding syntax toTextField. The editor shows and edits the string; every change reflects intextEditorTextimmediately..frame(height: 250)— Without this,TextEditorfills all available vertical space in theVStack, pushing the button off screen. Constraining height is almost always necessary when mixingTextEditorwith other views..colorMultiply(Color(#colorLiteral(...)))— The gray color literal (approximately#D5D5D5) is being multiplied with the editor's white background. White × light-gray = light-gray. This is a render-level trick, not a semantic background color. The comment.background(Color.red)above it shows the approach that doesn't work.savedText = textEditorText— A simple assignment captures the current editor value as a snapshot.savedTextdoesn't update again until the next Save tap. This pattern (live editing state vs. committed state) is the foundation for draft/publish workflows.
Common Mistakes
Mistake: Using .background(Color.someColor) on TextEditor and wondering why it has no effectTextEditor wraps a UITextView internally, and UITextView draws its own opaque background. SwiftUI's .background modifier doesn't penetrate this. Use .colorMultiply on iOS 14 and below, or .scrollContentBackground(.hidden) combined with .background on iOS 16+.
Mistake: Assuming TextEditor has placeholder support
Unlike TextField, TextEditor has no built-in placeholder. A common workaround is to overlay a Text("Placeholder...") with .opacity(textEditorText.isEmpty ? 1 : 0) on top of the editor, positioned at the top-leading edge.
Mistake: Not constraining TextEditor height in a VStackTextEditor greedily takes all available space. In a VStack with a button, it will push the button off-screen. Always set a .frame(height:) or use a Spacer strategically to keep the layout usable.
Key Takeaways
TextEditoris the SwiftUI view for multi-line, scrollable text input — use it for notes, bios, message composers, and any input longer than one line.TextEditorignores.background(color)— use.colorMultiply(color)on iOS 14 or.scrollContentBackground(.hidden)+.background(color)on iOS 16+.- Keeping a separate
savedTextstate distinct from the livetextEditorTextallows "draft then commit" workflows without extra complexity.
Last updated: June 27, 2026