'How to add placeholder text to TextEditor in SwiftUI?
When using SwiftUI's new TextEditor, you can modify its content directly using a @State. However, I haven't see a way to add a placeholder text to it. Is it doable right now?
I added an example that Apple used in their own translator app. Which appears to be a multiple lines text editor view that supports a placeholder text.
Solution 1:[1]
You can use a ZStack with a disabled TextEditor containing your placeholder text behind. For example:
ZStack {
if self.content.isEmpty {
TextEditor(text:$placeholderText)
.font(.body)
.foregroundColor(.gray)
.disabled(true)
.padding()
}
TextEditor(text: $content)
.font(.body)
.opacity(self.content.isEmpty ? 0.25 : 1)
.padding()
}
Solution 2:[2]
Until we have some API support, an option would be to use the binding string as placeholder and onTapGesture to remove it
TextEditor(text: self.$note)
.padding(.top, 20)
.foregroundColor(self.note == placeholderString ? .gray : .primary)
.onTapGesture {
if self.note == placeholderString {
self.note = ""
}
}
Solution 3:[3]
I built a custom view that can be used like this (until TextEditor officially supports it - maybe next year)
TextArea("This is my placeholder", text: $text)
Full solution below:
struct TextArea: View {
private let placeholder: String
@Binding var text: String
init(_ placeholder: String, text: Binding<String>) {
self.placeholder = placeholder
self._text = text
}
var body: some View {
TextEditor(text: $text)
.background(
HStack(alignment: .top) {
text.isBlank ? Text(placeholder) : Text("")
Spacer()
}
.foregroundColor(Color.primary.opacity(0.25))
.padding(EdgeInsets(top: 0, leading: 4, bottom: 7, trailing: 0))
)
}
}
extension String {
var isBlank: Bool {
return allSatisfy({ $0.isWhitespace })
}
}
I'm using the default padding of the TextEditor here, but feel free to adjust to your preference.
Solution 4:[4]
There are some good answers here, but I wanted to bring up a special case. When a TextEditor is placed in a Form, there are a few issues, primarily with spacing.
- TextEditor does not horizontally align with other form elements (e.g. TextField)
- The placeholder text does not horizontally align with the TextEditor cursor.
- When there is whitespace or carriage return/newline are added, the placeholder re-positions to the vertical-middle (optional).
- Adding leading spaces causes the placeholder to disappear (optional).
One way to fix these issues:
Form {
TextField("Text Field", text: $text)
ZStack(alignment: .topLeading) {
if comments.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
Text("Long Text Field").foregroundColor(Color(UIColor.placeholderText)).padding(.top, 8)
}
TextEditor(text: $comments).padding(.leading, -3)
}
}
Solution 5:[5]
With an overlay, you won't be able to allow touch on the placeholder text for the user to write in the textEditor. You better work on the background, which is a view.
So, create it, while deactivating the default background:
struct PlaceholderBg: View {
let text: String?
init(text:String? = nil) {
UITextView.appearance().backgroundColor = .clear // necessary to remove the default bg
self.text = text
}
var body: some View {
VStack {
HStack{
Text(text!)
Spacer()
}
Spacer()
}
}
}
then, in your textEditor:
TextEditor(text: $yourVariable)
.frame(width: x, y)
.background(yourVariable.isEmpty ? PlaceholderBg(texte: "my placeholder text") : PlaceholderBG(texte:""))
Solution 6:[6]
As I know, this is the best way to add a placeholder text to TextEditor in SwiftUI
struct ContentView: View {
@State var text = "Type here"
var body: some View {
TextEditor(text: self.$text)
// make the color of the placeholder gray
.foregroundColor(self.text == "Type here" ? .gray : .primary)
.onAppear {
// remove the placeholder text when keyboard appears
NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { (noti) in
withAnimation {
if self.text == "Type here" {
self.text = ""
}
}
}
// put back the placeholder text if the user dismisses the keyboard without adding any text
NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { (noti) in
withAnimation {
if self.text == "" {
self.text = "Type here"
}
}
}
}
}
}
Solution 7:[7]
I like Umayanga's approach but his code wasn't reusable. Here's the code as a reusable view:
struct TextEditorPH: View {
private var placeholder: String
@Binding var text: String
init(placeholder: String, text: Binding<String>) {
self.placeholder = placeholder
self._text = text
}
var body: some View {
TextEditor(text: self.$text)
// make the color of the placeholder gray
.foregroundColor(self.text == placeholder ? .gray : .primary)
.onAppear {
// create placeholder
self.text = placeholder
// remove the placeholder text when keyboard appears
NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { (noti) in
withAnimation {
if self.text == placeholder {
self.text = ""
}
}
}
// put back the placeholder text if the user dismisses the keyboard without adding any text
NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { (noti) in
withAnimation {
if self.text == "" {
self.text = placeholder
}
}
}
}
}
}
Solution 8:[8]
Combined with the answer of @grey, but with white background coverage, you need to remove the background to have an effect
struct TextArea: View {
private let placeholder: String
@Binding var text: String
init(_ placeholder: String, text: Binding<String>) {
self.placeholder = placeholder
self._text = text
// Remove the background color here
UITextView.appearance().backgroundColor = .clear
}
var body: some View {
TextEditor(text: $text)
.background(
HStack(alignment: .top) {
text.isBlank ? Text(placeholder) : Text("")
Spacer()
}
.foregroundColor(Color.primary.opacity(0.25))
.padding(EdgeInsets(top: 0, leading: 4, bottom: 7, trailing: 0))
)
}
}
extension String {
var isBlank: Bool {
return allSatisfy({ $0.isWhitespace })
}
}
Solution 9:[9]
SwiftUI TextEditor does not yet have support for a placeholder. As a result, we have to "fake" it.
Other solutions had problems like bad alignment or color issues. This is the closest I got to simulating a real placeholder. This solution "overlays" a TextField over the TextEditor. The TextField contains the placeholder. The TextField gets hidden as soon as a character is inputted into the TextEditor.
import SwiftUI
struct Testing: View {
@State private var textEditorText = ""
@State private var textFieldText = ""
var body: some View {
VStack {
Text("Testing Placeholder Example")
ZStack(alignment: Alignment(horizontal: .center, vertical: .top)) {
TextEditor(text: $textEditorText)
.padding(EdgeInsets(top: -7, leading: -4, bottom: -7, trailing: -4)) // fix padding not aligning with TextField
if textEditorText.isEmpty {
TextField("Placeholder text here", text: $textFieldText)
.disabled(true) // don't allow for it to be tapped
}
}
}
}
}
struct Testing_Previews: PreviewProvider {
static var previews: some View {
Testing()
}
}
Solution 10:[10]
I've read all the comments above (and in the Internet at all), combined some of them and decided to come to this solution:
- Create custom Binding wrapper
- Create TextEditor and Text with this binding
- Add some modifications to make all this pixel-perfect.
Let's start with creating wrapper:
extension Binding where Value: Equatable {
init(_ source: Binding<Value?>, replacingNilWith nilProxy: Value) {
self.init(
get: { source.wrappedValue ?? nilProxy },
set: { newValue in
if newValue == nilProxy {
source.wrappedValue = nil
} else {
source.wrappedValue = newValue
}
})
}
}
Next step is to initialize our binding as usual:
@State private var yourTextVariable: String?
After that put TextEditor and Text in the ZStack:
ZStack(alignment: .topLeading) {
Text(YOUR_HINT_TEXT)
.padding(EdgeInsets(top: 6, leading: 4, bottom: 0, trailing: 0))
.foregroundColor(.black)
.opacity(yourTextVariable == nil ? 1 : 0)
TextEditor(text: Binding($yourTextVariable, replacingNilWith: ""))
.padding(.all, 0)
.opacity(yourTextVariable != nil ? 1 : 0.8)
}
And this will give us pixel-perfect UI with needed functionality:
Solution 11:[11]
We can create a custom view to add placeholder text in the TextEditor.
Here is my solution:
AppTextEditor.swift
import SwiftUI
// MARK: - AppTextEditor
struct AppTextEditor: View {
@Binding var message: String
let placeholder: LocalizedStringKey
var body: some View {
ZStack(alignment: .topLeading) {
if message.isEmpty {
Text(placeholder)
.padding(8)
.font(.body)
.foregroundColor(Color.placeholderColor)
}
TextEditor(text: $message)
.frame(height: 100)
.opacity(message.isEmpty ? 0.25 : 1)
}
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.placeholderColor, lineWidth: 0.5))
}
}
// MARK: - AppTextEditor_Previews
struct AppTextEditor_Previews: PreviewProvider {
static var previews: some View {
AppTextEditor(message: .constant(""), placeholder: "Your Message")
.padding()
}
}
Color+Extensions.swift
extension Color {
static let placeholderColor = Color(UIColor.placeholderText)
}
Usage:
struct YourView: View {
@State var message = ""
var body: some View {
AppTextEditor(message: $message, placeholder: "Your message")
.padding()
}
}
Solution 12:[12]
With iOS 15, you can use FocusState in order to manage the focus state of a TextEditor.
The following code shows how to use FocusState in order to show or hide the placeholder of a TextEditor:
struct ContentView: View {
@State private var note = ""
@FocusState private var isNoteFocused: Bool
var body: some View {
Form {
ZStack(alignment: .topLeading) {
TextEditor(text: $note)
.focused($isNoteFocused)
if !isNoteFocused && note.isEmpty {
Text("Note")
.foregroundColor(Color(uiColor: .placeholderText))
.padding(.top, 10)
.allowsHitTesting(false)
}
}
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button("Done") {
isNoteFocused = false
}
}
}
}
}
Solution 13:[13]
I modified @bde.dev solution and here is the code sample and a screenshot..
struct TextEditorWithPlaceholder: View {
@Binding var text: String
var body: some View {
ZStack(alignment: .leading) {
if text.isEmpty {
VStack {
Text("Write something...")
.padding(.top, 10)
.padding(.leading, 6)
.opacity(0.6)
Spacer()
}
}
VStack {
TextEditor(text: $text)
.frame(minHeight: 150, maxHeight: 300)
.opacity(text.isEmpty ? 0.85 : 1)
Spacer()
}
}
}
}
And I used it in my view like:
struct UplodePostView: View {
@State private var text: String = ""
var body: some View {
NavigationView {
Form {
Section {
TextEditorWithPlaceholder(text: $text)
}
}
}
}
}
Solution 14:[14]
I did it this way:
TextEditor(text: $bindingVar)
.font(.title2)
.onTapGesture{
placeholderText = true
}
.frame(height: 150)
.overlay(
VStack(alignment: .leading){
HStack {
if !placeholderText {
Text("Your placeholdergoeshere")
.font(.title2)
.foregroundColor(.gray)
}
Spacer()
}
Spacer()
})
Solution 15:[15]
None of the suggested answers was helpful for me, When the user taps the TextEditor, it should hide the placeholder. Also there's a nasty bug from Apple that doesn't allow you to properly change the TextEditor's background color (iOS 15.5 time of writing this) I provided my refined code here.
Make sure add this code at the app initialization point:
@main
struct MyApplication1: App {
let persistenceController = PersistenceController.shared
init(){
UITextView.appearance().backgroundColor = .clear // <-- Make sure to add this line
}
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}
struct PlaceHolderTextEditor: View {
let cornerRadius:CGFloat = 8
let backgroundColor:Color = .gray
let placeholder: String
@Binding var text: String
@FocusState private var isFocused: Bool
var body: some View {
ZStack(alignment: Alignment(horizontal: .leading, vertical: .top)) {
TextEditor(text: $text)
.focused($isFocused)
.onChange(of: isFocused) { isFocused in
self.isFocused = isFocused
}
.opacity((text.isEmpty && !isFocused) ? 0.02 : 1)
.foregroundColor(.white)
.frame(height:150)
.background(backgroundColor)
if text.isEmpty && !isFocused {
Text(placeholder)
.padding(.top, 8)
.padding(.leading,8)
}
}.cornerRadius(cornerRadius)
}
}
Solution 16:[16]
Here is how I solved it.
I used a Text for the placeholder together with the TextEditor in a ZStack.
The first problem was that since the Text is opaque, it would prevent the TextEditor from becoming focused if you tapped on the area covered by the Text. Tapping on any other area would make the TextEditor focused.
So I solved it by adding a tap gesture with the new iOS 15 @FocusState property wrapper.
The second problem was that the TextEditor was not properly aligned to the left of the placeholder so I added a negative .leading padding to solve that.
struct InputView: View {
@State var text: String = ""
@FocusState var isFocused: Bool
var body: some View {
ZStack(alignment: .leading) {
TextEditor(text: $text)
.font(.body)
.padding(.leading, -4)
.focused($isFocused, equals: true)
if text.isEmpty {
Text("Placeholder text...")
.font(.body)
.foregroundColor(Color(uiColor: .placeholderText))
.onTapGesture {
self.isFocused = true
}
}
}
}
}
Hopefully it is natively supported in the future.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow


