How to add placeholder text to TextEditor in SwiftUI?

13,842

Solution 1

It is not possible out of the box but you can achieve this effect with ZStack or the .overlay property.

What you should do is check the property holding your state. If it is empty display your placeholder text. If it's not then display the inputted text instead.

And here is a code example:

ZStack(alignment: .leading) {
    if email.isEmpty {
        Text(Translation.email)
            .font(.custom("Helvetica", size: 24))
            .padding(.all)
    }
    
    TextEditor(text: $email)
        .font(.custom("Helvetica", size: 24))
        .padding(.all)
}

Note: I have purposely left the .font and .padding styling for you to see that it should match on both the TextEditor and the Text.

EDIT: Having in mind the two problems mentioned in Legolas Wang's comment here is how the alignment and opacity issues could be handled:

  • In order to make the Text start at the left of the view simply wrap it in HStack and append Spacer immediately after it like this:
HStack {
   Text("Some placeholder text")
   Spacer()
}
  • In order to solve the opaque problem you could play with conditional opacity - the simplest way would be using the ternary operator like this:
TextEditor(text: stringProperty)        
        .opacity(stringProperty.isEmpty ? 0.25 : 1)

Of course this solution is just a silly workaround until support gets added for TextEditors.

Solution 2

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 3

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 4

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 5

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.

  1. TextEditor does not horizontally align with other form elements (e.g. TextField)
  2. The placeholder text does not horizontally align with the TextEditor cursor.
  3. When there is whitespace or carriage return/newline are added, the placeholder re-positions to the vertical-middle (optional).
  4. 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)
    }
}
Share:
13,842
Legolas Wang
Author by

Legolas Wang

Updated on June 10, 2022

Comments

  • Legolas Wang
    Legolas Wang almost 2 years

    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?

    enter image description here

    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.

    • pawello2222
      pawello2222 almost 4 years
      I don't think it's possible now. It's still beta so it might change though.
    • Asperi
      Asperi almost 4 years
      I hardly believe it will be ever, it is TextEditor, not TextField. There was no placeholder in UITextView as well.
    • Legolas Wang
      Legolas Wang almost 4 years
      @Asperi I added an example from Apple's translator app, which seems to have a TextEditor view that supports placeholder. I'm trying to achieve the same.
    • Asperi
      Asperi almost 4 years
      key word is seems ... looks at this solution How do I create a multiline TextField in SwiftUI?
    • ricardopereira
      ricardopereira almost 4 years
      I created a Feedback Assistant asking for this be available in the final Xcode 12 release 🙏 (FB8118309)
  • Legolas Wang
    Legolas Wang almost 4 years
    It is a brilliant thought, but unfortunately it suffered from two problems. The first one is the TextEditor view, which is opaque, so it will block the placeholder view when layering on top in a ZStack. Tweaking with opacity could help a little in this case. The second problem is the frame logic with Text and TextEditor, The TextEditor begin from left top corner, and the Text starts from the center of the view. Which makes them very hard to overlay exactly on top. Do you have some thoughts about the alignment issue?
  • bde.dev
    bde.dev almost 4 years
    @LegolasWang I didn't want to include anything super specific about styling but instead left the font and padding only in order to demonstrate that the styling, aligning etc. should match. I am adding an edit to my answer to demonstrate how those 2 for-mentioned problems could be handled.
  • RndmTsk
    RndmTsk over 3 years
    You can actually put the HStack below the TextEditor and give it a .contentShape of NoShape: ``` struct NoShape: Shape { func path(in rect: CGRect) -> Path { return Path() } } // ... HStack { Text("Some placeholder text") .contentShape(NoShape()) } ```
  • unixb0y
    unixb0y over 3 years
    somehow, there's a white plane overlaying the placeholder 🤔
  • grey
    grey over 3 years
    Still using this on iOS 14.2 (light and dark mode) and no issues so far. If you're using it with other custom views though, you might want to change the code a bit to suit your needs. Feel free to share your screenshot and code though 😊
  • SwiftUser
    SwiftUser over 3 years
    The day where you can use a TextEditor a dismiss the keyboard, similar to a TextField is the day I rejoice.
  • qwerty-reloader
    qwerty-reloader over 2 years
    For placeholder text color you can use: .foregroundColor(Color(UIColor.placeholderText))
  • Richard Topchii
    Richard Topchii over 2 years
    What if the user types placeholder string?
  • LilaQ
    LilaQ about 2 years
    Please don't do it like that, this can bring all sorts of problems.