ios 如何在 SwiftUI 中创建多行 TextField?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/56471973/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-31 09:53:39  来源:igfitidea点击:

How do I create a multiline TextField in SwiftUI?

iosswiftui

提问by gabriellanata

I've been trying to create a multiline TextField in SwiftUI, but I can't figure out how.

我一直在尝试在 SwiftUI 中创建一个多行 TextField,但我不知道如何。

This is the code I currently have:

这是我目前拥有的代码:

struct EditorTextView : View {
    @Binding var text: String

    var body: some View {
        TextField($text)
            .lineLimit(4)
            .multilineTextAlignment(.leading)
            .frame(minWidth: 100, maxWidth: 200, minHeight: 100, maxHeight: .infinity, alignment: .topLeading)
    }
}

#if DEBUG
let sampleText = """
Very long line 1
Very long line 2
Very long line 3
Very long line 4
"""

struct EditorTextView_Previews : PreviewProvider {
    static var previews: some View {
        EditorTextView(text: .constant(sampleText))
            .previewLayout(.fixed(width: 200, height: 200))
    }
}
#endif

But this is the output:

但这是输出:

enter image description here

在此处输入图片说明

采纳答案by sas

Update: While Xcode11 beta 4 now does support TextView, I've found that wrapping a UITextViewis still be best way to get editable multiline text to work. For instance, TextViewhas display glitches where text does not appear properly inside the view.

更新:虽然 Xcode11 beta 4 现在确实支持TextView,但我发现包装 aUITextView仍然是使可编辑的多行文本工作的最佳方式。例如,TextView具有显示故障,其中文本在视图内无法正确显示。

Original (beta 1) answer:

原始(测试版 1)答案:

For now, you could wrap a UITextViewto create a composable View:

现在,您可以包装 aUITextView以创建一个可组合的View

import SwiftUI
import Combine

final class UserData: BindableObject  {
    let didChange = PassthroughSubject<UserData, Never>()

    var text = "" {
        didSet {
            didChange.send(self)
        }
    }

    init(text: String) {
        self.text = text
    }
}

struct MultilineTextView: UIViewRepresentable {
    @Binding var text: String

    func makeUIView(context: Context) -> UITextView {
        let view = UITextView()
        view.isScrollEnabled = true
        view.isEditable = true
        view.isUserInteractionEnabled = true
        return view
    }

    func updateUIView(_ uiView: UITextView, context: Context) {
        uiView.text = text
    }
}

struct ContentView : View {
    @State private var selection = 0
    @EnvironmentObject var userData: UserData

    var body: some View {
        TabbedView(selection: $selection){
            MultilineTextView(text: $userData.text)
                .tabItemLabel(Image("first"))
                .tag(0)
            Text("Second View")
                .font(.title)
                .tabItemLabel(Image("second"))
                .tag(1)
        }
    }
}

#if DEBUG
struct ContentView_Previews : PreviewProvider {
    static var previews: some View {
        ContentView()
            .environmentObject(UserData(
                text: """
                        Some longer text here
                        that spans a few lines
                        and runs on.
                        """
            ))

    }
}
#endif

enter image description here

在此处输入图片说明

回答by Asperi

Ok, I started with @sas approach, but needed it really look&feel as multi-line text field with content fit, etc. Here is what I've got. Hope it will be helpful for somebody else... Used Xcode 11.1.

好的,我从 @sas 方法开始,但需要它真正的外观和感觉作为具有内容适合等的多行文本字段。这是我所拥有的。希望对其他人有帮助......使用Xcode 11.1。

Provided custom MultilineTextField has:
1. content fit
2. autofocus
3. placeholder
4. on commit

提供的自定义 MultilineTextField 具有:
1. 内容适合
2. 自动对焦
3. 占位符
4. 提交时

Preview of swiftui multiline textfield with content fitAdded placeholder

swiftui 多行文本字段的预览,内容适合添加占位符

import SwiftUI
import UIKit

fileprivate struct UITextViewWrapper: UIViewRepresentable {
    typealias UIViewType = UITextView

    @Binding var text: String
    @Binding var calculatedHeight: CGFloat
    var onDone: (() -> Void)?

    func makeUIView(context: UIViewRepresentableContext<UITextViewWrapper>) -> UITextView {
        let textField = UITextView()
        textField.delegate = context.coordinator

        textField.isEditable = true
        textField.font = UIFont.preferredFont(forTextStyle: .body)
        textField.isSelectable = true
        textField.isUserInteractionEnabled = true
        textField.isScrollEnabled = false
        textField.backgroundColor = UIColor.clear
        if nil != onDone {
            textField.returnKeyType = .done
        }

        textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
        return textField
    }

    func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<UITextViewWrapper>) {
        if uiView.text != self.text {
            uiView.text = self.text
        }
        if uiView.window != nil, !uiView.isFirstResponder {
            uiView.becomeFirstResponder()
        }
        UITextViewWrapper.recalculateHeight(view: uiView, result: $calculatedHeight)
    }

    fileprivate static func recalculateHeight(view: UIView, result: Binding<CGFloat>) {
        let newSize = view.sizeThatFits(CGSize(width: view.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
        if result.wrappedValue != newSize.height {
            DispatchQueue.main.async {
                result.wrappedValue = newSize.height // !! must be called asynchronously
            }
        }
    }

    func makeCoordinator() -> Coordinator {
        return Coordinator(text: $text, height: $calculatedHeight, onDone: onDone)
    }

    final class Coordinator: NSObject, UITextViewDelegate {
        var text: Binding<String>
        var calculatedHeight: Binding<CGFloat>
        var onDone: (() -> Void)?

        init(text: Binding<String>, height: Binding<CGFloat>, onDone: (() -> Void)? = nil) {
            self.text = text
            self.calculatedHeight = height
            self.onDone = onDone
        }

        func textViewDidChange(_ uiView: UITextView) {
            text.wrappedValue = uiView.text
            UITextViewWrapper.recalculateHeight(view: uiView, result: calculatedHeight)
        }

        func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
            if let onDone = self.onDone, text == "\n" {
                textView.resignFirstResponder()
                onDone()
                return false
            }
            return true
        }
    }

}

struct MultilineTextField: View {

    private var placeholder: String
    private var onCommit: (() -> Void)?

    @Binding private var text: String
    private var internalText: Binding<String> {
        Binding<String>(get: { self.text } ) {
            self.text = 
import SwiftUI

struct ContentView: View {
     @State var text = ""

       var body: some View {
        VStack {
            Text("text is: \(text)")
            TextView(
                text: $text
            )
                .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
        }

       }
}

struct TextView: UIViewRepresentable {
    @Binding var text: String

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIView(context: Context) -> UITextView {

        let myTextView = UITextView()
        myTextView.delegate = context.coordinator

        myTextView.font = UIFont(name: "HelveticaNeue", size: 15)
        myTextView.isScrollEnabled = true
        myTextView.isEditable = true
        myTextView.isUserInteractionEnabled = true
        myTextView.backgroundColor = UIColor(white: 0.0, alpha: 0.05)

        return myTextView
    }

    func updateUIView(_ uiView: UITextView, context: Context) {
        uiView.text = text
    }

    class Coordinator : NSObject, UITextViewDelegate {

        var parent: TextView

        init(_ uiTextView: TextView) {
            self.parent = uiTextView
        }

        func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
            return true
        }

        func textViewDidChange(_ textView: UITextView) {
            print("text now: \(String(describing: textView.text!))")
            self.parent.text = textView.text
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
self.showingPlaceholder =
import SwiftUI
import TextView

struct ContentView: View {
    @State var input = ""
    @State var isEditing = false

    var body: some View {
        VStack {
            Button(action: {
                self.isEditing.toggle()
            }) {
                Text("\(isEditing ? "Stop" : "Start") editing")
            }
            TextView(text: $input, isEditing: $isEditing)
        }
    }
}
.isEmpty } } @State private var dynamicHeight: CGFloat = 100 @State private var showingPlaceholder = false init (_ placeholder: String = "", text: Binding<String>, onCommit: (() -> Void)? = nil) { self.placeholder = placeholder self.onCommit = onCommit self._text = text self._showingPlaceholder = State<Bool>(initialValue: self.text.isEmpty) } var body: some View { UITextViewWrapper(text: self.internalText, calculatedHeight: $dynamicHeight, onDone: onCommit) .frame(minHeight: dynamicHeight, maxHeight: dynamicHeight) .background(placeholderView, alignment: .topLeading) } var placeholderView: some View { Group { if showingPlaceholder { Text(placeholder).foregroundColor(.gray) .padding(.leading, 4) .padding(.top, 8) } } } } #if DEBUG struct MultilineTextField_Previews: PreviewProvider { static var test:String = ""//some very very very long description string to be initially wider than screen" static var testBinding = Binding<String>(get: { test }, set: { // print("New value: \(
struct TextView: UIViewRepresentable {
    var placeholder: String
    @Binding var text: String

    var minHeight: CGFloat
    @Binding var calculatedHeight: CGFloat

    init(placeholder: String, text: Binding<String>, minHeight: CGFloat, calculatedHeight: Binding<CGFloat>) {
        self.placeholder = placeholder
        self._text = text
        self.minHeight = minHeight
        self._calculatedHeight = calculatedHeight
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()
        textView.delegate = context.coordinator

        // Decrease priority of content resistance, so content would not push external layout set in SwiftUI
        textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)

        textView.isScrollEnabled = false
        textView.isEditable = true
        textView.isUserInteractionEnabled = true
        textView.backgroundColor = UIColor(white: 0.0, alpha: 0.05)

        // Set the placeholder
        textView.text = placeholder
        textView.textColor = UIColor.lightGray

        return textView
    }

    func updateUIView(_ textView: UITextView, context: Context) {
        textView.text = self.text

        recalculateHeight(view: textView)
    }

    func recalculateHeight(view: UIView) {
        let newSize = view.sizeThatFits(CGSize(width: view.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
        if minHeight < newSize.height && $calculatedHeight.wrappedValue != newSize.height {
            DispatchQueue.main.async {
                self.$calculatedHeight.wrappedValue = newSize.height // !! must be called asynchronously
            }
        } else if minHeight >= newSize.height && $calculatedHeight.wrappedValue != minHeight {
            DispatchQueue.main.async {
                self.$calculatedHeight.wrappedValue = self.minHeight // !! must be called asynchronously
            }
        }
    }

    class Coordinator : NSObject, UITextViewDelegate {

        var parent: TextView

        init(_ uiTextView: TextView) {
            self.parent = uiTextView
        }

        func textViewDidChange(_ textView: UITextView) {
            // This is needed for multistage text input (eg. Chinese, Japanese)
            if textView.markedTextRange == nil {
                parent.text = textView.text ?? String()
                parent.recalculateHeight(view: textView)
            }
        }

        func textViewDidBeginEditing(_ textView: UITextView) {
            if textView.textColor == UIColor.lightGray {
                textView.text = nil
                textView.textColor = UIColor.black
            }
        }

        func textViewDidEndEditing(_ textView: UITextView) {
            if textView.text.isEmpty {
                textView.text = parent.placeholder
                textView.textColor = UIColor.lightGray
            }
        }
    }
}
)") test =
struct ContentView: View {
    @State var text: String = ""
    @State var textHeight: CGFloat = 150

    var body: some View {
        ScrollView {
            TextView(placeholder: "", text: self.$text, minHeight: self.textHeight, calculatedHeight: self.$textHeight)
            .frame(minHeight: self.textHeight, maxHeight: self.textHeight)
        }
    }
}
} ) static var previews: some View { VStack(alignment: .leading) { Text("Description:") MultilineTextField("Enter some text here", text: testBinding, onCommit: { print("Final text: \(test)") }) .overlay(RoundedRectangle(cornerRadius: 4).stroke(Color.black)) Text("Something static here...") Spacer() } .padding() } } #endif

回答by Meo Flute

This wraps UITextView in Xcode Version 11.0 beta 6 (still working at Xcode 11 GM seed 2):

这将 UITextView 包装在 Xcode 版本 11.0 beta 6 中(仍在 Xcode 11 GM 种子 2 中工作):

struct TextView: UIViewRepresentable {

    typealias UIViewType = UITextView
    var configuration = { (view: UIViewType) in }

    func makeUIView(context: UIViewRepresentableContext<Self>) -> UIViewType {
        UIViewType()
    }

    func updateUIView(_ uiView: UIViewType, context: UIViewRepresentableContext<Self>) {
        configuration(uiView)
    }
}

回答by Andrew Ebling

With a Text()you can achieve this using .lineLimit(nil), and the documentation suggests this shouldwork for TextField()too. However, I can confirm this does not currently work as expected.

使用 aText()可以实现这一点.lineLimit(nil),并且文档表明这也应该适用TextField()。但是,我可以确认这目前没有按预期工作。

I suspect a bug - would recommend filing a report with Feedback Assistant. I have done this and the ID is FB6124711.

我怀疑有错误 - 建议使用反馈助手提交报告。我已经这样做了,ID 是 FB6124711。

回答by Ken Mueller

Currently, the best solution is to use this package I created called TextView.

目前,最好的解决方案是使用我创建的名为TextView 的包。

You can install it using Swift Package Manager (explained in the README). It allows for toggle-able editing state, and numerous customizations (also detailed in the README).

您可以使用 Swift Package Manager(在 README 中解释)安装它。它允许切换编辑状态和大量自定义(也在自述文件中详细说明)。

Here's an example:

下面是一个例子:

struct ContentView: View {
    var body: some View {
        TextView() {
            ##代码##.textColor = .red
            // Any other setup you like
        }
    }
}

In that example, you first define two @Statevariables. One is for the text, which the TextView writes to whenever it is typed in, and another is for the isEditingstate of the TextView.

在该示例中,您首先定义两个@State变量。一个用于文本,TextView 在输入时写入文本,另一个用于isEditingTextView的状态。

The TextView, when selected, toggles the isEditingstate. When you click the button, that also toggles the isEditingstate which will show the keyboard and select the TextView when true, and deselect the TextView when false.

TextView 在被选中时会切换isEditing状态。当您单击按钮时,还会切换isEditing状态,该状态将显示键盘并在 时选择 TextView true,并在 时取消选择 TextView false

回答by Daniel Tseng

@Meo Flute's answer is great! But it doesn't work for multistage text input. And combined with @Asperi's answer, here is the fixed for that and I also added the support for placeholder just for fun!

@Meo Flute 的回答很棒!但它不适用于多级文本输入。并结合@Asperi 的回答,这是修复的问题,我还添加了对占位符的支持,只是为了好玩!

##代码##

Use it like this:

像这样使用它:

##代码##

回答by Mojtaba Hosseini

Using Native UITextView

使用原生 UITextView

you can use the native UITextView right in the SwiftUI code with this struct:

您可以在 SwiftUI 代码中使用本机 UITextView 使用此结构:

##代码##

Usage

用法

##代码##

Advantages: - Tested in for years in UIKit- Native solution - Scrollable - Fully customizable - All other benefits of the original UITextView

优点: - 经过多年的测试UIKit- 原生解决方案 - 可滚动 - 完全可定制 - 原始版本的所有其他优点UITextView