如何在SwiftUI中创建多行TextField?


85

我一直在尝试在SwiftUI中创建多行TextField,但是我不知道怎么做。

这是我目前拥有的代码:

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

但这是输出:

在此处输入图片说明


1
我只是尝试使用lineLimit()在GM的Xcode版本11.0(11A419c)中使用swiftui创建多行文本字段。它仍然不起作用。我不敢相信苹果还没有解决这个问题。多行文本字段在移动应用程序中相当普遍。
e987

Answers:


45

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

原始(测试版1)答案:

现在,您可以包装UITextView以创建一个composable 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

在此处输入图片说明


伟大的临时解决!现在接受,直到可以使用纯SwiftUI解决为止。
gabriellanata

7
此解决方案使您可以显示已包含换行符的文本,但似乎不会自然地打断/换行。(文本仅在框架外部的一行上保持水平增长。)有什么想法可以使长行换行吗?
迈克尔

5
如果我使用State(而不是Publisher的EnvironmentObject)并将其作为绑定传递给MultilineTextView,则它似乎不起作用。我如何将更改反映回州?
灰色的

有没有办法在不使用environmentObject的情况下在textview中设置默认文本?
Learn2Code

76

好的,我从@sas方法开始,但实际上需要它看起来像具有内容适合的多行文本字段,等等。这就是我所拥有的。希望对其他人有所帮助。使用过Xcode 11.1。

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

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

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 = $0
            self.showingPlaceholder = $0.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: \($0)")
        test = $0 } )

    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

6
您还应该考虑backgroundColor将UITextField设置为UIColor.clear使用SwiftUI启用自定义背景,并考虑删除auto-firstresponder,因为MultilineTextFields在一个视图中使用多个时,它会中断(每次击键,所有文本字段都会尝试再次获取响应者)。
iComputerfreak

2
@ kdion4891正如在另一个问题中的答复所解释的那样,您可以执行textField.textContainerInset = UIEdgeInsets.zero+textField.textContainer.lineFragmentPadding = 0并可以正常使用👌🏻@Asperi如果您按照上述说明进行操作,则需要将其删除.padding(.leading, 4).padding(.top, 8)否则它将看起来很破损。此外,您可以更改.foregroundColor(.gray).foregroundColor(Color(UIColor.tertiaryLabel))给占位符颜色匹配TextFieldS(我没有检查是否有暗模式正在更新)。
雷米B.

3
哦,而且,我也更改@State private var dynamicHeight: CGFloat = 100@State private var dynamicHeight: CGFloat = UIFont.systemFontSizeMultilineTextField出现时修复一个小的“小故障” (短时间显示大故障,然后缩小)。
雷米B.

2
@ q8yas,您可以注释或删除与uiView.becomeFirstResponder
Asperi '19

3
谢谢大家的评论!我非常感谢。提供的快照是方法的演示,该方法是为特定目的配置的。您的所有建议都是正确的,但出于您的目的。您可以随意复制并粘贴此代码,并根据需要进行尽可能多的重新配置。
阿斯佩里

28

这将UITextView包装在Xcode 11.0 beta 6中(仍在Xcode 11 GM种子2上运行):

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

1
在Xcode版本11.0(11A420a),GM种子2、2019
e987

2
这在VStack中效果很好,但是当使用List时,行的高度不会扩展以显示TextView中的所有文本。我试了几件事情:改变isScrollEnabledTextView执行情况; 在TextView框架上设置固定宽度;甚至将TextView和Text放在ZStack中(希望该行将扩展为与Text视图的高度匹配),但是没有任何效果。有人对如何调整此答案使其也适用于列表有任何建议吗?
MathewS

@Meo Flute可以使高度与内容匹配。
阿卜杜拉

我将isScrollEnabled更改为false,谢谢。
阿卜杜拉

26

有了Text()你可以做到这一点使用.lineLimit(nil),并且该文档表明,这应该为之工作TextField()过。但是,我可以确认这当前无法正常工作。

我怀疑有一个错误-建议向反馈助手提交报告。我已经做到了,ID是FB6124711。

编辑:针对iOS 14的更新:改用新的TextEditor


有没有一种方法可以使用ID FB6124711搜索错误?当我检查反馈助手时,它并没有太大帮助
CrazyPro007

我不认为有办法做到这一点。但是您可以在报告中提及该ID,解释您的ID是对同一问题的重复。这有助于分类小组提高问题的优先级。
Andrew Ebling

2
确认这仍然是Xcode 11.0 beta 2(11M337n)中的问题
Andrew Ebling

3
确认这仍然是Xcode 11.0 beta 3(11M362v)中的问题。您可以将字符串设置为“ Some \ ntext”,它将显示在两行中,但是键入新内容只会导致一行文本在视图框架之外水平增长。
迈克尔

3
这仍然是Xcode 11.4中的问题-严重吗???我们应该如何在生产中使用类似这样的bug的SwiftUI。
Trev14

16

iOS 14

叫做 TextEditor

struct ContentView: View {
    @State var text: String = "Multiline \ntext \nis called \nTextEditor"

    var body: some View {
        TextEditor(text: $text)
    }
}

动态生长高度:

如果您希望它随着键入而增长,请在其上嵌入如下标签:

ZStack {
    TextEditor(text: $text)
    Text(text).opacity(0).padding(.all, 8) // <- This will solve the issue if it is in the same ZStack
}

演示版

演示版


iOS 13

使用本机UITextView

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

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

用法

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

好处:

  • 支持iOS 13
  • 与旧版代码共享
  • 经过多年测试 UIKit
  • 完全可定制
  • 原件的所有其他好处 UITextView

3
如果有人在看这个答案,并且想知道如何将实际文本传递到TextView结构,则在设置textColor的行下面添加以下行:$ 0.text =“ Some text”
Mattl

1
您如何将文本绑定到变量?还是检索文本?
biomiker

1
第一个选项已经具有文本绑定。第二个是标准UITextView。您可以像通常在UIKit中那样与它进行交互。
Mojtaba Hosseini

12

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

您可以使用Swift Package Manager(自述文件中对此进行了解释)进行安装。它允许可切换的编辑状态和许多自定义项(在README中也有详细介绍)。

这是一个例子:

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

在该示例中,首先定义两个@State变量。一个用于文本,每次键入时都会将文本视图写入文本,另一个用于文本isEditing状态。

选择TextView时,将切换isEditing状态。当您点击按钮时,这也会切换isEditing显示键盘状态,并在时选择TextView,在时true取消选择TextView false


1
我将在存储库中添加一个问题,但它与Asperi的原始解决方案存在类似的问题,它在VStack中效果很好,但在ScrollView中却效果不佳。
RogerTheShrubber '19

No such module 'TextView'
AlexBartiş

编辑:您的目标是macOS,但由于UIViewRepresentable,该框架仅支持UIKit
AlexBartiş

10

@Meo Flute的答案很好!但这不适用于多阶段文本输入。并结合@Asperi的答案,这里已解决了该问题,我还添加了对占位符的支持,只是为了好玩!

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

像这样使用它:

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

我喜欢这个。占位符似乎没有用,但从头开始很有用。我建议使用UIColor.tertiaryLabel等语义颜色代替UIColor.lightGray和UIColor.label代替UIColor.black,以便同时支持亮和暗模式。
希拉姆

@Helam您介意解释占位符如何不起作用?
曾俊华

@DanielTseng它没有出现。它应该如何表现?我期望它显示文本是否为空,但从不显示。
Helam

@Helam,在我的示例中,我的占位符为空。您是否尝试过将其更改为其他内容?(“世界,您好!”而不是“”)
曾俊伟

是的,在我看来,我将其设置为其他内容。
Helam

2

SwiftUI TextView(UIViewRepresentable)具有以下可用参数:fontStyle,isEditable,backgroundColor,borderColor和border Width

TextView(文本:self。$ viewModel.text,fontStyle:.body,isEditable:true,backgroundColor:UIColor.white,borderColor:UIColor.lightGray,borderWidth:1.0).padding()

TextView(UIViewRepresentable)

struct TextView: UIViewRepresentable {

@Binding var text: String
var fontStyle: UIFont.TextStyle
var isEditable: Bool
var backgroundColor: UIColor
var borderColor: UIColor
var borderWidth: CGFloat

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

func makeUIView(context: Context) -> UITextView {

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

    myTextView.font = UIFont.preferredFont(forTextStyle: fontStyle)
    myTextView.isScrollEnabled = true
    myTextView.isEditable = isEditable
    myTextView.isUserInteractionEnabled = true
    myTextView.backgroundColor = backgroundColor
    myTextView.layer.borderColor = borderColor.cgColor
    myTextView.layer.borderWidth = borderWidth
    myTextView.layer.cornerRadius = 8
    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) {
        self.parent.text = textView.text
    }
}

}


1

适用于Xcode 12iOS14,非常简单。

import SwiftUI

struct ContentView: View {
    
    @State private var text = "Hello world"
    
    var body: some View {
        TextEditor(text: $text)
    }
}

仅当您使用iOS14时,这才不是吗,如果用户仍在iOS13上怎么办
Di Nerd

1

MacOS实施

struct MultilineTextField: NSViewRepresentable {
    
    typealias NSViewType = NSTextView
    private let textView = NSTextView()
    @Binding var text: String
    
    func makeNSView(context: Context) -> NSTextView {
        textView.delegate = context.coordinator
        return textView
    }
    func updateNSView(_ nsView: NSTextView, context: Context) {
        nsView.string = text
    }
    func makeCoordinator() -> Coordinator {
        return Coordinator(self)
    }
    class Coordinator: NSObject, NSTextViewDelegate {
        let parent: MultilineTextField
        init(_ textView: MultilineTextField) {
            parent = textView
        }
        func textDidChange(_ notification: Notification) {
            guard let textView = notification.object as? NSTextView else { return }
            self.parent.text = textView.string
        }
    }
}

以及如何使用

struct ContentView: View {

@State var someString = ""

    var body: some View {
         MultilineTextField(text: $someString)
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.