如何在 SwiftUI 中创建多行 TextField?

我一直试图在 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

但这就是结果:

enter image description here

94684 次浏览

使用 Text(),您可以使用 .lineLimit(nil)实现这一点,文档建议这个 应该也适用于 TextField()。然而,我可以确认,这不是目前的工作预期。

我怀疑是一个错误——建议你向反馈助理提交一份报告。我已经这样做了,ID 是 FB6124711。

编辑: iOS14更新: 改用新的 TextEditor

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

原始答案(beta 1) :

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

这将 UITextView 包装在 Xcode Version 11.0 beta 6(仍然在 Xcode 11GM 籽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()
}
}

好的,我开始使用@sas 方法,但是需要它看起来和感觉起来像多行文本字段,内容适合等等。这是我的发现。希望对其他人有所帮助... 用过的 Xcode 11.1。

假设自定义 MultilineTextField 具有:
1. 内容适合
2. 自动对焦
3. 占位符
4. 承诺

Preview of swiftui multiline textfield with content fit Added placeholder

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

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

您可以使用 Swift Package Manager 安装它(在 README 中解释)。它支持可切换的编辑状态和许多定制(在 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变量。一个是针对文本的,TextView 在输入文本时会对其进行写入,另一个是针对 TextView 的 isEditing状态的。

选择 TextView 时,将切换 isEditing状态。当你点击按钮,这也切换 isEditing状态将显示键盘和选择文本视图时 true,并取消选择文本视图时 false

IOS 16-beta 操作系统

可以将 TextField配置为使用新的 axis参数垂直展开。此外,它使用 lineLimit修饰符来限制给定范围内的行:

TextField("Title", text: $text,  axis: .vertical)
.lineLimit(5...10)

但是 lineLimit 修饰符现在也支持更高级的行为,比如保留最小的空间并随着内容的增加而扩展,然后在内容超过上限时滚动


IOS14和15-本地 SwiftUI

它叫做 TextEditor

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


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

动态生长高度:

如果你想让它在你输入的时候增长,把它嵌入到 ZStack中,像这样的 Text:

Demo


IOS13-使用 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
}
}
}

优点:

  • IOS13的支持
  • 与遗留代码共享
  • UIKit测试多年
  • 完全可定制
  • UITextView的所有其他优点

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

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

import SwiftUI


struct ContentView: View {
    

@State private var text = "Hello world"
    

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

带有以下可用参数的 SwiftUI TextView (UIViewExpresable) : 可编辑,背景色,边框颜色和边框宽度

TextView (text: self. $viewModel.text,fontStyle: . body,isEditable: true,background Color: UIColor.white,borderColor: UIColor.lightGray,borderWidth: 1.0) 。填充()

TextView (用户可视表示)

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

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

您可以只使用 TextEditor(text: $text),然后添加诸如高度之类的任何修饰符。

SwiftUI 有 TextEditor,类似于 TextField,但是提供了长形式的文本输入,可以包装成多行:

var body: some View {
NavigationView{
Form{
Section{
List{
Text(question6)
TextEditor(text: $responseQuestion6).lineLimit(4)
Text(question7)
TextEditor(text:  $responseQuestion7).lineLimit(4)
}
}
}
}
}

这是我根据 Asperi 的回答得出的结论。这个解决方案 不需要计算和传播大小。它在 TextView内部使用 contentSizeintrinsicContentSize:

resizable text view

struct TextView: UIViewRepresentable {
@Binding var text: String
    

func makeUIView(context: UIViewRepresentableContext<TextView>) -> UITextView {
let textView = UIKitTextView()
        

textView.delegate = context.coordinator
        

return textView
}
    

func updateUIView(_ textView: UITextView, context: UIViewRepresentableContext<TextView>) {
if textView.text != self.text {
textView.text = self.text
}
}
    

func makeCoordinator() -> Coordinator {
return Coordinator(text: $text)
}
    

final private class UIKitTextView: UITextView {
override var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
        

override var intrinsicContentSize: CGSize {
// Or use e.g. `min(contentSize.height, 150)` if you want to restrict max height
CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
}
    

final class Coordinator: NSObject, UITextViewDelegate {
var text: Binding<String>
        

init(text: Binding<String>) {
self.text = text
}
        

func textViewDidChange(_ textView: UITextView) {
text.wrappedValue = textView.text
}
}
}
// You can use the .multiLineTextAlignment modifier


TextField("Random text", text: $text)
.multiLineTextAlignment(.leading)


// This aligns the text to the left
// There are more properties beside '.leading', more can be found at the source

只是想分享我的 UITextView解决方案减去协调器。我注意到 SwiftUI 调用 UITextView.intrinsicContentSize时并没有告诉它应该适合哪个宽度。默认情况下,UITextView假设它有无限的宽度来布局内容,所以如果它只有一行文本,它将返回适合该行所需的大小。

为了解决这个问题,我们可以子类 UITextView并在视图的宽度改变时使内部大小失效,并在计算内部大小时考虑到宽度。

struct TextView: UIViewRepresentable {


var text: String


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


public func makeUIView(context: Context) -> UITextView {
let textView = WrappedTextView()
textView.backgroundColor = .clear
textView.isScrollEnabled = false
textView.textContainerInset = .zero
textView.textContainer.lineFragmentPadding = 0
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
textView.setContentHuggingPriority(.defaultHigh, for: .vertical)
return textView
}


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


class WrappedTextView: UITextView {


private var lastWidth: CGFloat = 0


override func layoutSubviews() {
super.layoutSubviews()
if bounds.width != lastWidth {
lastWidth = bounds.width
invalidateIntrinsicContentSize()
}
}


override var intrinsicContentSize: CGSize {
let size = sizeThatFits(
CGSize(width: lastWidth, height: UIView.layoutFittingExpandedSize.height))
return CGSize(width: size.width.rounded(.up), height: size.height.rounded(.up))
}
}

screenrecord

我使用 textEditor

        TextEditor(text: $text)
.multilineTextAlignment(.leading)
.cornerRadius(25)
.font(Font.custom("AvenirNext-Regular", size: 20, relativeTo: .body))
//.autocapitalization(.words)
.disableAutocorrection(true)
.border(Color.gray, width: 3)
.padding([.leading, .bottom, .trailing])
        

我想我应该分享我的代码,因为其他的答案没有正确使用 Coordinator:

struct UITextViewTest: View {
@State var text = "Hello, World!"
var body: some View {
VStack {
TextField("", text: $text)
MultilineTextField(text: $text)
}
}
}




struct MultilineTextField: UIViewRepresentable {
@Binding var text: String
    

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


func makeUIView(context: Context) -> UITextView {
context.coordinator.textView
}
    

func updateUIView(_ uiView: UITextView, context: Context) {
// update in case the text value has changed, we assume the UIView checks if the value is different before doing any actual work.
// fortunately UITextView doesn't call its delegate when setting this property (in case of MKMapView, we would need to set our did change closures to nil to prevent infinite loop).
uiView.text = text


// since the binding passed in may have changed we need to give a new closure to the coordinator.
context.coordinator.textDidChange = { newText in
text = newText
}
}
    

class Coordinator: NSObject, UITextViewDelegate {
lazy var textView: UITextView = {
let textView = UITextView()
textView.font = .preferredFont(forTextStyle: .body)
textView.delegate = self
return textView
}()
        

var textDidChange: ((String) -> Void)?
        

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

func textViewDidChange(_ textView: UITextView) {
textDidChange?(textView.text)
}
}
}