¿Cómo creo un TextField de varias líneas en SwiftUI?

85

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:

ingrese la descripción de la imagen aquí

Gabriellanata
fuente
1
Intenté crear un campo de texto de varias líneas con swiftui en Xcode Versión 11.0 (11A419c), el GM, usando lineLimit (). Todavía no funciona. No puedo creer que Apple no haya solucionado esto todavía. Un campo de texto de varias líneas es bastante común en las aplicaciones móviles.
e987

Respuestas:

45

Actualización: si bien Xcode11 beta 4 ahora es compatible TextView, he descubierto que ajustar un UITextViewsigue siendo la mejor manera de hacer que el texto editable de varias líneas funcione. Por ejemplo, TextViewtiene fallas de visualización donde el texto no aparece correctamente dentro de la vista.

Respuesta original (beta 1):

Por ahora, puede ajustar un UITextViewpara crear un componible 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

ingrese la descripción de la imagen aquí

sas
fuente
¡Gran solución temporal! Aceptando por ahora hasta que se pueda resolver usando SwiftUI puro.
gabriellanata
7
Esta solución le permite mostrar texto que ya tiene nuevas líneas, pero no parece romper / ajustar líneas naturalmente largas. (El texto sigue creciendo horizontalmente en una línea, fuera del marco). ¿Alguna idea de cómo hacer líneas largas para ajustar?
Michael
5
Si uso State (en lugar de EnvironmentObject con un Publisher) y lo paso como un enlace a MultilineTextView, no parece funcionar. ¿Cómo puedo reflejar los cambios en el estado?
gris
¿Hay alguna forma de establecer un texto predeterminado en la vista de texto sin usar un EnvironmentObject?
Learn2Code
77

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

Vista previa del campo de texto multilínea swiftui con ajuste de contenido Marcador de posición agregado

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
Asperi
fuente
6
También debe pensar en configurar el backgroundColorUITextField para UIColor.clearhabilitar fondos personalizados usando SwiftUI y en eliminar el primer respondedor automático, porque se rompe cuando se usan múltiples MultilineTextFieldsen una vista (cada pulsación de tecla, todos los campos de texto intentan obtener el respondedor nuevamente).
iComputerfreak
2
@ kdion4891 Como se explicó en esta respuesta de otra pregunta , puede hacer textField.textContainerInset = UIEdgeInsets.zero+ textField.textContainer.lineFragmentPadding = 0y 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 en TextFields (no verifiqué si se está actualizando con el modo oscuro).
Rémi B.
3
Ah, y también cambié @State private var dynamicHeight: CGFloat = 100para @State private var dynamicHeight: CGFloat = UIFont.systemFontSizecorregir un pequeño "error" cuando MultilineTextFieldaparece (se muestra grande por un corto tiempo y luego se encoge).
Rémi B.
2
@ q8yas, puede comentar o eliminar el código relacionado conuiView.becomeFirstResponder
Asperi
3
¡Gracias a todos por los comentarios! Realmente aprecio eso. La instantánea proporcionada es una demostración del enfoque, que se configuró para un propósito específico. Todas sus propuestas son correctas, pero para sus propósitos. Puede copiar y pegar este código y reconfigurarlo tanto como desee para su propósito.
Asperi
28

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()
    }
}
Flauta meo
fuente
1
TextField aún no se ve afectado por lineLimit () en Xcode Versión 11.0 (11A420a), GM Seed 2, septiembre de 2019
e987
2
Esto funciona bien en una VStack, pero cuando se usa una List, la altura de la fila no se expande para mostrar todo el texto en TextView. He intentado algunas cosas: cambiar isScrollEnabledla TextViewimplementació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?
MathewS
@Meo Flute está ahí para hacer que la altura coincida con el contenido.
Abdullah
He cambiado isScrollEnabled a false y funciona, gracias.
Abdullah
26

Con un Text(), puede lograr esto usando .lineLimit(nil), y la documentación sugiere que esto también debería funcionar TextField(). 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 TextEditorlugar.

Andrew Ebling
fuente
¿Hay alguna manera de buscar el error con la identificación FB6124711? Como estoy revisando el asistente de retroalimentación, pero no es muy útil
CrazyPro007
No creo que haya una forma de hacer eso. Pero podría mencionar esa identificación en su informe, explicando que la suya es un engaño del mismo problema. Esto ayuda al equipo de triaje a plantear la prioridad del problema.
Andrew Ebling
2
Confirmado que esto sigue siendo un problema en Xcode versión 11.0 beta 2 (11M337n)
Andrew Ebling
3
Confirmado que esto sigue siendo un problema en Xcode versión 11.0 beta 3 (11M362v). Puede establecer la cadena en "Algún \ ntexto" y se mostrará en dos líneas, pero escribir contenido nuevo solo hará que una línea de texto crezca horizontalmente, fuera del marco de su vista.
Michael
3
Esto sigue siendo un problema en Xcode 11.4 - ¿En serio ??? ¿Cómo se supone que vamos a usar SwiftUI en producción con errores como este?
Trev14
16

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

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:

  • Soporte para iOS 13
  • Compartido con el código heredado
  • Probado durante años en UIKit
  • Totalmente personalizable
  • Todos los demás beneficios del original UITextView
Mojtaba Hosseini
fuente
3
Si alguien está mirando esta respuesta y se pregunta cómo pasar el texto real a la estructura TextView, agregue la siguiente línea debajo de la que establece el color de texto: $ 0.text = "Some text"
Mattl
1
¿Cómo enlazas el texto a una variable? ¿O recuperar el texto?
biomiker
1
La primera opción ya tiene el enlace de texto. El segundo es un estándar UITextView. Puede interactuar con él como lo hace habitualmente en UIKit.
Mojtaba Hosseini
12

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 @Statevariables. Uno es para el texto, en el que escribe TextView cada vez que se escribe, y otro es para el isEditingestado de TextView.

TextView, cuando se selecciona, cambia el isEditingestado. Cuando hace clic en el botón, también cambia el isEditingestado que mostrará el teclado y seleccionará TextView cuando true, y anule la selección de TextView cuando false.

Ken Mueller
fuente
1
Agregaré un problema en el repositorio, pero tiene un problema similar a la solución original de Asperi, funciona muy bien en un VStack, pero no en un ScrollView.
RogerTheShrubber
No such module 'TextView'
Alex Bartiş
Editar: está apuntando a macOS pero el marco solo es compatible con UIKit debido a UIViewRepresentable
Alex Bartiş
10

¡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)
        }
    }
}
Daniel Tseng
fuente
Me gusta esto. El marcador de posición no parece funcionar, pero fue útil comenzar. Sugiero usar colores semánticos como UIColor.tertiaryLabel en lugar de UIColor.lightGray y UIColor.label en lugar de UIColor.black para que tanto el modo claro como el oscuro sean compatibles.
Helam
@Helam ¿Te importaría explicar cómo no funciona el marcador de posición?
Daniel Tseng
@DanielTseng no aparece. ¿Cómo se supone que debe comportarse? Esperaba que se mostrara si el texto está vacío, pero nunca me lo muestra.
Helam
@Helam, en mi ejemplo, tengo el marcador de posición vacío. ¿Ha intentado cambiarlo por otra cosa? ("¡Hola mundo!" En lugar de "")
Daniel Tseng
Sí, en el mío lo configuré para que fuera otra cosa.
Helam
2

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

}

Di Nerd
fuente
1

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)
    }
}
Gandhi Mena
fuente
¿No es esto solo si estás trabajando con iOS14, qué pasa si el usuario todavía está en iOS13
Di Nerd
1

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)
    }
}
Denis Rybkin
fuente