Implementing Placeholder Text in SwiftUI TextEditor
The TextEditor view introduced in iOS 14 is a powerful tool for multiline, scrollable text entry. However, unlike TextField, it lacks a native API for placeholder text.
Here is a clean, modern way to implement a custom placeholder using a ZStack and conditional logic.
- Setting the Foundation
To make our placeholder look native, we first need to handle the TextEditor background. By default, it has an opaque background that hides whatever is behind it. In earlier versions of iOS 14, we use the UITextView appearance proxy to clear this background.
struct ContentView: View {
@State private var text = ""
init() {
// Remove the default background to see our custom ZStack background
UITextView.appearance().backgroundColor = .clear
}
var body: some View {
ZStack(alignment: .topLeading) {
// Our custom background
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(Color(UIColor.secondarySystemBackground))
TextEditor(text: $text)
.padding(4)
}
.frame(width: 300, height: 400)
.font(.body)
}
}
- Adding the Placeholder Logic
To display the placeholder, we check if the text string is empty. If it is, we overlay a Text view.
Critical Placement: We place the placeholder before the TextEditor in the ZStack. This ensures the placeholder sits behind the editor, allowing the TextEditor to intercept all tap gestures while remaining visible through the transparent background.
struct ContentView: View {
@State private var text = ""
init() {
UITextView.appearance().backgroundColor = .clear
}
var body: some View {
ZStack(alignment: .topLeading) {
// 1. Background Layer
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(Color(UIColor.secondarySystemBackground))
// 2. Placeholder Layer (Only shown when empty)
if text.isEmpty {
Text("Type something here...")
.foregroundColor(Color(UIColor.placeholderText))
.padding(.horizontal, 8)
.padding(.vertical, 12)
// Allows taps to pass through to the TextEditor below
.allowsHitTesting(false)
}
// 3. Editor Layer
TextEditor(text: $text)
.padding(4)
}
.frame(width: 300, height: 400)
.font(.body)
}
}
Key Considerations
Padding: Notice the slight difference in padding between the Text and TextEditor. This is necessary to align the placeholder text perfectly with the cursor position inside the editor.
Colors: Using Color(UIColor.placeholderText) ensures your UI stays consistent with system-wide accessibility and dark mode settings.
Hit Testing: While the ZStack order usually handles it, adding .allowsHitTesting(false) to the placeholder is a best practice to ensure it never interferes with the user’s ability to focus the editor.