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?

One example is the text input in Apple's own translator app. Which seems like a multiple lines text editor view that supports a placeholder text.


Post not yet marked as solved Up vote post of SeaOverload Down vote post of SeaOverload
12k views

Replies

Code Block
struct ContentView: View {
@State private var yourText: String = "YOUR PLACEHOLDER TEXT"
var body: some View {
TextEditor(text: $yourText)
}
}


This solution is adapted from responses (primarily Mojtaba Hosseini) to a couple of similar questions on StackOverflow and also from Alan Quatermain's well written blog titled "SwiftUI Bindings with CoreData".

I use this solution in a macOS target (with Core Data) and an @ObservedObject var. I've adapted this answer to work independently with an @State var. Please be aware that I have not attempted to build and run this with an iOS target yet, but it works in the SwiftUI canvas in Xcode 12 beta 1 and using live preview for the macOS target.

Essentially TextEditor and Text views must be drawn in the same way - must have identical characteristics, e.g. .font modifier, etc. The Text view "ghosts" the TextEditor and performs two roles:
  1. presents the placeholder text when the Optional<String> (@State private var objectDescription: String?) is nil.

  2. dynamically expands the height of the stacked views to accommodate multiple lines of text in the TextEditor.

If anyone understands the mechanics behind what makes the "ghosting" technique work in the ZStack, please add an answer, because I'm still trying to figure that out.

Note that TextEditor seems to be a little unstable in Xcode 12 beta 1 under macOS 11 beta 2. I include the .frame modifier to ensure the TextEditor view presents itself as I expect it to - without this modifier it occasionally and inexplicably "disappears".

Code Block
public 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
                }
        })
    }
}


Code Block
struct YourSwiftUIView: View {
@State private var objectDescription: String?
var body: some View {
VStack(alignment: .leading) {
            let placeholder = "enter detailed Description"
            Text("Description")
          ZStack(alignment: .topLeading) {
                TextEditor(text: Binding($objectDescription, replacingNilWith: ""))
                    .frame(minHeight: 30, alignment: .leading)
// following line is a hack to force TextEditor to appear
// similar to .textFieldStyle(RoundedBorderTextFieldStyle())...
                    .cornerRadius(6.0)
                    .foregroundColor(Color(.labelColor))
                    .multilineTextAlignment(.leading)
                Text(objectDescription ?? placeholder)
// following line is a hack to create an inset similar to the TextEditor inset...
                    .padding(.leading, 5)
                    .foregroundColor(Color(.placeholderTextColor))
                    .opacity(objectDescription == nil ? 1 : 0)
            }
            .font(.body)
        }
}
}

Until we get placeholder support from TextEditor API, I'm using something like this:

Code Block swift
TextEditor(text: self.$note)
          .foregroundColor(self.note == placeholderString ? .gray : .primary)
          .onTapGesture {
            if self.note == placeholderString {
              self.note = ""
            }
          }


As I know, this is the best way to add a placeholder text to TextEditor in SwiftUI

Code Block
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"
}
}
}
}
}
}


Here's my take on the ZStack method:

ZStack(alignment: .topLeading) {
    TextEditor(text: $draftPreface)
        .frame(height: 100)
        .border(Color(uiColor: .opaqueSeparator), width: 0.5)
    Text("preface").fontWeight(.light).foregroundColor(.black.opacity(0.25)).padding(8).hidden(!draftPreface.isEmpty)
}

I extended View with a conditional .hidden:

extension View {
    @ViewBuilder public func hidden(_ shouldHide: Bool) -> some View {
        switch shouldHide {
        case true: self.hidden()
        case false: self
        }
    }
}

What I do is adding a background with the placeholder in the position I want and observing the changes in the TextEditor text. So when it's empty, I show the background (placeholder) and when it's not, I hide it.

The only addition I would suggest on top of ZStack is to add allowHitTesting(false) modifier to the Text, so that clicking on the placeholder also sets focus onto the TextEditor

TextEditor(text: $msg)
                .overlay {
                    if msg == "" {
                        HStack{
                            Text("write message")
                                .foregroundStyle(Color.secondary)
                            Spacer()
                        }.padding(.horizontal)
                    }
                }