59

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.

Legolas Wang
  • 1,951
  • 1
  • 13
  • 26
  • I don't think it's possible now. It's still beta so it might change though. – pawello2222 Jul 05 '20 at 14:33
  • 1
    I hardly believe it will be ever, it is TextEditor, not TextField. There was no placeholder in UITextView as well. – Asperi Jul 05 '20 at 14:34
  • @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. – Legolas Wang Jul 05 '20 at 15:04
  • key word is *seems* ... looks at this solution [How do I create a multiline TextField in SwiftUI?](https://stackoverflow.com/a/58639072/12299030) – Asperi Jul 05 '20 at 15:08
  • 1
    I created a Feedback Assistant asking for this be available in the final Xcode 12 release (FB8118309) – ricardopereira Jul 23 '20 at 19:56

20 Answers20

49

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.

bde.dev
  • 729
  • 9
  • 9
  • 1
    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? – Legolas Wang Jul 06 '20 at 07:24
  • @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. – bde.dev Jul 06 '20 at 17:28
  • 1
    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()) } ``` – RndmTsk Oct 08 '20 at 22:24
  • 3
    For placeholder text color you can use: .foregroundColor(Color(UIColor.placeholderText)) – qwerty-reloader Oct 27 '21 at 11:02
33

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()
}
Magnus
  • 411
  • 5
  • 4
13

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.

ygee
  • 2,580
  • 3
  • 17
  • 21
  • somehow, there's a white plane overlaying the placeholder – unixb0y Nov 14 '20 at 23:07
  • 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 – ygee Nov 16 '20 at 01:06
  • 1
    The day where you can use a TextEditor a dismiss the keyboard, similar to a TextField is the day I rejoice. – SwiftUser Dec 11 '20 at 17:46
12

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 = ""
                    }
                }
mawus
  • 1,178
  • 1
  • 11
  • 25
12

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)
                    }
                }
            }
        }
    }

Output

cs4alhaider
  • 1,318
  • 1
  • 17
  • 26
6

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
                }
            }
        }
    }

}
Imanou Petit
  • 89,880
  • 29
  • 256
  • 218
  • 4
    I believe this to be most superior and most up-to-date version of all solutions here in 2022! +1 on FocusState and +1 on using .allowsHitTesting(false) – Peter Lapisu Oct 31 '22 at 08:57
3

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)
    }
}
Mykel
  • 1,355
  • 15
  • 25
3

Native Solution from iOS 16

Instead of TextEditor use TextField. Since iOS 16 it supports multiple lines and of course place holder natively, when using one of the initializers with the axis parameter.

TextField("Placeholder...", text: .constant("hey"), axis: .vertical)
    .lineLimit(1...5)

With the range on you can even say how big the TextField can get at minimum. Or use linelimit(_:reservesSpace:).

You may missing some features of TextEditor but for me it's working perfectly.

heyfrank
  • 5,291
  • 3
  • 32
  • 46
  • This solution is only suitable if you initially have 1-line text (which is no more than 1 line high). Otherwise, your placeholder will be in the middle (vertically) of the field-box, which doesn't look good – zslavman Aug 02 '23 at 17:16
2

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:""))
Dharman
  • 30,962
  • 25
  • 85
  • 135
NightCoder
  • 1,049
  • 14
  • 22
2

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 })
    }
}
gaohomway
  • 2,132
  • 1
  • 20
  • 37
1

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"
                        }
                    }
                }
            }
    }
}
umayanga
  • 2,254
  • 3
  • 15
  • 26
1

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
                        }
                    }
                }
            }
    }
}
Thofreym
  • 59
  • 6
1

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.

Oluwatobi Omotayo
  • 1,719
  • 14
  • 28
0

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()
  }
}
kgaidis
  • 14,259
  • 4
  • 79
  • 93
0

I've read all the comments above (and in the Internet at all), combined some of them and decided to come to this solution:

  1. Create custom Binding wrapper
  2. Create TextEditor and Text with this binding
  3. 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:

https://youtu.be/T1TcSWo-Mtc

Izya Pitersky
  • 106
  • 1
  • 6
0

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()
  }
}
sant05
  • 418
  • 7
  • 21
0

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()
                    })
0

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)
    }
}
Amir.n3t
  • 2,859
  • 3
  • 21
  • 28
0

textEditor{...}.onTapGesture {

                if text == placeholder {
                    self.text = ""
                }
                
           
            }.onAppear {
                text = placeholder
            }
        
          
        

        Button {
            
            text = placeholder
            isFocused = false
            
        }....
  • 1
    As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Sep 01 '22 at 05:06
0

Fighting TextEditor recently I use this as an approximate and simple solution

        TextEditor(text: dvbEventText)
          .overlay(alignment:.topLeading) 
             {
              Text(dvbEventText.wrappedValue.count == 0 ? "Enter Event Text":"")
                .foregroundColor(Color.lightGray)
                .disabled(true)
             }

As soon as you start typing the hint goes away and the prompt text is where you type.

FWIW

altimes
  • 380
  • 2
  • 13