简体   繁体   English

SwiftUI中TextEditor如何添加占位符文本?

[英]How to add placeholder text to TextEditor in SwiftUI?

When using SwiftUI's new TextEditor, you can modify its content directly using a @State.使用 SwiftUI 的新 TextEditor 时,您可以直接使用 @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.我添加了一个 Apple 在他们自己的翻译应用程序中使用的示例。 Which appears to be a multiple lines text editor view that supports a placeholder text.这似乎是一个支持占位符文本的多行文本编辑器视图。

It is not possible out of the box but you can achieve this effect with ZStack or the .overla y property.开箱即用是不可能的,但您可以使用ZStack.overla y 属性来实现此效果。

What you should do is check the property holding your state.你应该做的是检查持有你的 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.注意:我特意留下了 .font 和 .padding 样式供您查看它应该在 TextEditor 和 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:编辑:记住 Legolas Wang 的评论中提到的两个问题是如何处理 alignment 和不透明度问题:

  • 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 和 append Spacer 中,如下所示:
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.当然,在添加对 TextEditors 的支持之前,此解决方案只是一个愚蠢的解决方法。

You can use a ZStack with a disabled TextEditor containing your placeholder text behind.您可以将 ZStack 与禁用的TextEditor一起使用,其中包含您的占位符文本。 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()
}

Until we have some API support, an option would be to use the binding string as placeholder and onTapGesture to remove it在我们有一些 API 支持之前,一个选项是使用绑定字符串作为占位符并使用 onTapGesture 删除它

TextEditor(text: self.$note)
                .padding(.top, 20)
                .foregroundColor(self.note == placeholderString ? .gray : .primary)
                .onTapGesture {
                    if self.note == placeholderString {
                        self.note = ""
                    }
                }

I built a custom view that can be used like this (until TextEditor officially supports it - maybe next year)我构建了一个可以像这样使用的自定义视图(直到 TextEditor 正式支持它 - 也许明年)

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.我在这里使用TextEditor的默认填充,但可以根据您的喜好随意调整。

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 放置在 Form 中时,会出现一些问题,主要是间距问题。

  1. TextEditor does not horizontally align with other form elements (eg TextField) TextEditor 不与其他表单元素水平对齐(例如 TextField)
  2. The placeholder text does not horizontally align with the TextEditor cursor.占位符文本未与 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)
    }
}

With an overlay, you won't be able to allow touch on the placeholder text for the user to write in the textEditor.使用覆盖,您将无法允许触摸占位符文本以供用户在 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:""))

As I know, this is the best way to add a placeholder text to TextEditor in SwiftUI据我所知,这是在 SwiftUI 中向 TextEditor 添加占位符文本的最佳方式

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

I like Umayanga's approach but his code wasn't reusable.我喜欢 Umayanga 的方法,但他的代码不可重用。 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
                        }
                    }
                }
            }
    }
}

Combined with the answer of @grey, but with white background coverage, you need to remove the background to have an effect结合@grey的回答,但是有白色背景覆盖,需要去掉背景才有效果

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

SwiftUI TextEditor does not yet have support for a placeholder. SwiftUI TextEditor尚不支持占位符。 As a result, we have to "fake" it.结果,我们不得不“伪造”它。

Other solutions had problems like bad alignment or color issues.其他解决方案存在问题,例如 alignment 损坏或颜色问题。 This is the closest I got to simulating a real placeholder.这是我最接近模拟真实占位符的方法。 This solution "overlays" a TextField over the TextEditor .该解决方案在TextEditor上“覆盖”了一个TextField The TextField contains the placeholder. TextField包含占位符。 The TextField gets hidden as soon as a character is inputted into the TextEditor .一旦将字符输入到TextEditor中, TextField就会被隐藏。

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

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使用此绑定创建 TextEditor 和 Text
  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:之后将 TextEditor 和 Text 放入 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:这将为我们提供具有所需功能的像素完美 UI:

https://youtu.be/T1TcSWo-Mtc https://youtu.be/T1TcSWo-Mtc

We can create a custom view to add placeholder text in the TextEditor.我们可以创建一个自定义视图来在 TextEditor 中添加占位符文本。

Here is my solution:这是我的解决方案:

AppTextEditor.swift 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颜色+扩展。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()
  }
}

With iOS 15, you can use FocusState in order to manage the focus state of a TextEditor .使用 iOS 15,您可以使用FocusState来管理TextEditor的焦点 state。

The following code shows how to use FocusState in order to show or hide the placeholder of a TextEditor :以下代码展示了如何使用FocusState来显示或隐藏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
                }
            }
        }
    }

}

I modified @bde.dev solution and here is the code sample and a screenshot..我修改了@bde.dev 解决方案,这里是代码示例和屏幕截图..

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

输出

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

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.还有一个来自 Apple 的令人讨厌的错误,它不允许您正确更改 TextEditor 的背景颜色(撰写本文时 iOS 15.5)我在此处提供了我的精炼代码。

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

Here is how I solved it.这是我解决它的方法。

I used a Text for the placeholder together with the TextEditor in a ZStack .我在ZStack中使用Text作为占位符以及TextEditor

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 .第一个问题是,由于Text是不透明的,如果您点击Text覆盖的区域,它将阻止TextEditor成为焦点。 Tapping on any other area would make the TextEditor focused.点击任何其他区域将使 TextEditor 聚焦。 So I solved it by adding a tap gesture with the new iOS 15 @FocusState property wrapper.所以我通过使用新的 iOS 15 @FocusState属性包装器添加轻击手势来解决它。

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.第二个问题是 TextEditor 没有正确对齐占位符的左侧,所以我添加了一个负的.leading填充来解决这个问题。

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.希望它在未来得到本机支持。

textEditor{...}.onTapGesture {文本编辑器{...}.onTapGesture {

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

        Button {
            
            text = placeholder
            isFocused = false
            
        }....

Fighting TextEditor recently I use this as an approximate and simple solution最近大战TextEditor 我用这个作为一个近似简单的解决方案

        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 FWIW

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM