He estado intentando crear un TextField de varias líneas en SwiftUI, pero no puedo entender cómo.
Este es el código que tengo actualmente:
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
Pero esta es la salida:
Respuestas:
Actualización: si bien Xcode11 beta 4 ahora es compatible
TextView
, he descubierto que ajustar unUITextView
sigue siendo la mejor manera de hacer que el texto editable de varias líneas funcione. Por ejemplo,TextView
tiene fallas de visualización donde el texto no aparece correctamente dentro de la vista.Respuesta original (beta 1):
Por ahora, puede ajustar un
UITextView
para crear un componibleView
: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
fuente
Ok, comencé con el enfoque @sas, pero necesitaba que realmente se viera y se sintiera como un campo de texto de varias líneas con ajuste de contenido, etc. Esto es lo que tengo. Espero que sea útil para alguien más ... Usé Xcode 11.1.
El campo MultilineTextField personalizado proporcionado tiene:
1. ajuste de contenido
2. enfoque automático
3. marcador de posición
4. en la confirmación
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
fuente
backgroundColor
UITextField paraUIColor.clear
habilitar fondos personalizados usando SwiftUI y en eliminar el primer respondedor automático, porque se rompe cuando se usan múltiplesMultilineTextFields
en una vista (cada pulsación de tecla, todos los campos de texto intentan obtener el respondedor nuevamente).textField.textContainerInset = UIEdgeInsets.zero
+textField.textContainer.lineFragmentPadding = 0
y funciona bien 👌🏻 @Asperi Si hace lo mencionado, deberá eliminarlo.padding(.leading, 4)
y, de lo.padding(.top, 8)
contrario, se verá roto. Además, puede cambiar.foregroundColor(.gray)
para.foregroundColor(Color(UIColor.tertiaryLabel))
que coincida con el color de los marcadores de posición enTextField
s (no verifiqué si se está actualizando con el modo oscuro).@State private var dynamicHeight: CGFloat = 100
para@State private var dynamicHeight: CGFloat = UIFont.systemFontSize
corregir un pequeño "error" cuandoMultilineTextField
aparece (se muestra grande por un corto tiempo y luego se encoge).uiView.becomeFirstResponder
Esto envuelve UITextView en Xcode Versión 11.0 beta 6 (todavía funciona en Xcode 11 GM seed 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() } }
fuente
isScrollEnabled
laTextView
implementación; establecer un ancho fijo en el marco TextView; e incluso poner TextView y Text en un ZStack (con la esperanza de que la fila se expandiera para igualar la altura de la vista Text) pero nada funciona. ¿Alguien tiene algún consejo sobre cómo adaptar esta respuesta para que también funcione en una lista?Con un
Text()
, puede lograr esto usando.lineLimit(nil)
, y la documentación sugiere que esto también debería funcionarTextField()
. Sin embargo, puedo confirmar que esto no funciona actualmente como se esperaba.Sospecho que hay un error, recomendaría presentar un informe con Feedback Assistant. He hecho esto y la identificación es FB6124711.
EDITAR: Actualización para iOS 14: use el nuevo en su
TextEditor
lugar.fuente
iOS 14
Se llama
TextEditor
struct ContentView: View { @State var text: String = "Multiline \ntext \nis called \nTextEditor" var body: some View { TextEditor(text: $text) } }
Altura de crecimiento dinámica:
Si desea que crezca a medida que escribe, incrústelo con una etiqueta como la siguiente:
ZStack { TextEditor(text: $text) Text(text).opacity(0).padding(.all, 8) // <- This will solve the issue if it is in the same ZStack }
Manifestación
iOS 13
Uso de Native UITextView
puede usar el UITextView nativo directamente en el código SwiftUI con esta estructura:
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) } }
Uso
struct ContentView: View { var body: some View { TextView() { $0.textColor = .red // Any other setup you like } } }
Ventajas:
UIKit
UITextView
fuente
UITextView
. Puede interactuar con él como lo hace habitualmente en UIKit.Actualmente, la mejor solución es usar este paquete que creé llamado TextView .
Puede instalarlo usando Swift Package Manager (explicado en el archivo README). Permite cambiar el estado de edición y numerosas personalizaciones (también detalladas en el archivo README).
He aquí un ejemplo:
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) } } }
En ese ejemplo, primero define dos
@State
variables. Uno es para el texto, en el que escribe TextView cada vez que se escribe, y otro es para elisEditing
estado de TextView.TextView, cuando se selecciona, cambia el
isEditing
estado. Cuando hace clic en el botón, también cambia elisEditing
estado que mostrará el teclado y seleccionará TextView cuandotrue
, y anule la selección de TextView cuandofalse
.fuente
No such module 'TextView'
¡La respuesta de @Meo Flute es genial! Pero no funciona para la entrada de texto de varias etapas. Y combinado con la respuesta de @ Asperi, aquí está el arreglo para eso y también agregué el soporte para el marcador de posición solo por diversión.
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 } } } }
Úselo así:
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) } } }
fuente
SwiftUI TextView (UIViewRepresentable) con los siguientes parámetros disponibles: fontStyle, isEditable, backgroundColor, borderColor y border Width
TextView (texto: 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 } }
}
fuente
Disponible para Xcode 12 e iOS14 , es realmente fácil.
import SwiftUI struct ContentView: View { @State private var text = "Hello world" var body: some View { TextEditor(text: $text) } }
fuente
Implementación de 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 } } }
y como usar
struct ContentView: View { @State var someString = "" var body: some View { MultilineTextField(text: $someString) } }
fuente