Come creo un TextField multilinea in SwiftUI?


93

Ho provato a creare un TextField multilinea in SwiftUI, ma non riesco a capire come.

Questo è il codice che ho attualmente:

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

Ma questo è l'output:

inserisci qui la descrizione dell'immagine


1
Ho appena provato a creare un campo di testo multilinea con swiftui in Xcode versione 11.0 (11A419c), il GM, usando lineLimit (). Ancora non funziona. Non posso credere che Apple non l'abbia ancora risolto. Un campo di testo multilinea è abbastanza comune nelle app mobili.
e987

Risposte:


49

Aggiornamento: sebbene Xcode11 beta 4 ora supporti TextView, ho scoperto che il wrapping di un UITextViewè ancora il modo migliore per far funzionare il testo multilinea modificabile. Ad esempio, TextViewpresenta problemi di visualizzazione in cui il testo non viene visualizzato correttamente all'interno della vista.

Risposta originale (beta 1):

Per ora, puoi avvolgere un UITextViewper creare un componibile 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

inserisci qui la descrizione dell'immagine


Grande soluzione temporanea! Accetto per ora fino a quando non può essere risolto utilizzando SwiftUI puro.
gabriellanata

7
Questa soluzione ti consente di visualizzare il testo che ha già delle nuove righe, ma non sembra spezzare / mandare a capo naturalmente le righe lunghe. (Il testo continua a crescere orizzontalmente su una riga, fuori dal riquadro.) Qualche idea su come avvolgere le righe lunghe?
Michael

6
Se uso State (invece di un EnvironmentObject con un Publisher) e lo passo come associazione a MultilineTextView, non sembra funzionare. Come posso riportare le modifiche allo Stato?
grigio

C'è un modo per impostare un testo predefinito nella visualizzazione di testo senza utilizzare un environmentObject?
Learn2Code

83

Ok, ho iniziato con l'approccio @sas, ma avevo bisogno che avesse un aspetto e un aspetto davvero come un campo di testo multilinea con contenuto adatto, ecc. Ecco cosa ho. Spero che possa essere utile per qualcun altro ... Usato Xcode 11.1.

Il MultilineTextField personalizzato fornito ha:
1. contenuto adatto
2. autofocus
3. segnaposto
4. su commit

Anteprima del campo di testo multilinea swiftui con adattamento del contenuto Segnaposto aggiunto

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
Inoltre dovresti pensare di impostare l' backgroundColorUITextField per UIColor.clearabilitare sfondi personalizzati usando SwiftUI e rimuovere l'auto-firstresponder, perché si interrompe quando si usano più MultilineTextFieldsin una vista (ogni battitura, tutti i campi di testo cercano di ottenere di nuovo il risponditore).
iComputerfreak

2
@ kdion4891 Come spiegato in questa risposta da un'altra domanda , puoi semplicemente fare textField.textContainerInset = UIEdgeInsets.zero+ textField.textContainer.lineFragmentPadding = 0e funziona bene 👌🏻 @Asperi Se fai come accennato, dovrai quindi rimuoverlo .padding(.leading, 4)e .padding(.top, 8)altrimenti sembrerà rotto. Inoltre, potresti cambiare .foregroundColor(.gray)per .foregroundColor(Color(UIColor.tertiaryLabel))per abbinare il colore dei segnaposto in TextFields (non ho controllato se si sta aggiornando con la modalità scura).
Rémi B.

3
Oh, e ho anche cambiato @State private var dynamicHeight: CGFloat = 100per @State private var dynamicHeight: CGFloat = UIFont.systemFontSizecorreggere un piccolo "glitch" quando MultilineTextFieldappare (si mostra grande per un breve periodo e poi si restringe).
Rémi B.

2
@ q8yas, puoi commentare o rimuovere il codice relativo auiView.becomeFirstResponder
Asperi

3
Grazie a tutti per i commenti! Lo apprezzo davvero. L'istantanea fornita è una demo dell'approccio, che è stato configurato per uno scopo specifico. Tutte le tue proposte sono corrette, ma per i tuoi scopi. Sei libero di copiare e incollare questo codice e riconfigurarlo quanto vuoi per il tuo scopo.
Asperi

31

Con a Text()puoi ottenere questo risultato .lineLimit(nil)e la documentazione suggerisce che dovrebbe funzionare TextField()anche per questo. Tuttavia, posso confermare che attualmente non funziona come previsto.

Sospetto un bug: consiglierei di presentare una segnalazione con Feedback Assistant. L'ho fatto e l'ID è FB6124711.

EDIT: Aggiornamento per iOS 14: usa TextEditorinvece il nuovo .


C'è un modo per cercare il bug usando l'id FB6124711? Dato che sto controllando l'assistente di feedback ma non è molto utile
CrazyPro007

Non credo che ci sia un modo per farlo. Ma potresti menzionare quell'ID nel tuo rapporto, spiegando che il tuo è un duplicato dello stesso problema. Questo aiuta il team di triage ad aumentare la priorità del problema.
Andrew Ebling

2
Confermato che questo è ancora un problema nella versione Xcode 11.0 beta 2 (11M337n)
Andrew Ebling

3
Confermato che questo è ancora un problema nella versione Xcode 11.0 beta 3 (11M362v). Puoi impostare la stringa su "Some \ ntext" e verrà visualizzata su due righe, ma la digitazione di un nuovo contenuto farà sì che una riga di testo cresca orizzontalmente, fuori dalla cornice della tua vista.
Michael

3
Questo è ancora un problema in Xcode 11.4 - Sul serio ??? Come dovremmo usare SwiftUI in produzione con bug come questo.
Trev14

29

Questo avvolge UITextView nella versione Xcode 11.0 beta 6 (ancora funzionante su 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()
    }
}

1
TextField non è ancora influenzato da lineLimit () in Xcode versione 11.0 (11A420a), GM Seed 2, settembre 2019
e987

2
Funziona bene in un VStack, ma quando si utilizza un List l'altezza della riga non si espande per mostrare tutto il testo in TextView. Ho provato un paio di cose: cambiare isScrollEnablednella TextViewattuazione; impostare una larghezza fissa sul frame TextView; e persino mettere TextView e Text in uno ZStack (nella speranza che la riga si espandesse per corrispondere all'altezza della visualizzazione Text) ma non funziona nulla. Qualcuno ha dei consigli su come adattare questa risposta per lavorare anche in una lista?
MathewS

@Meo Flute è disponibile per far corrispondere l'altezza al contenuto.
Abdullah

Ho cambiato isScrollEnabled in false e funziona, grazie.
Abdullah

28

iOS 14

È chiamato TextEditor

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

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

Altezza di crescita dinamica:

Se vuoi che cresca durante la digitazione, incorporalo con un'etichetta come di seguito:

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

Demo

Demo


iOS 13

Utilizzando Native UITextView

puoi utilizzare la UITextView nativa direttamente nel codice SwiftUI con questa struttura:

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

Utilizzo

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

Vantaggi:

  • Supporto per iOS 13
  • Condiviso con il codice legacy
  • Testato per anni in UIKit
  • Completamente personalizzabile
  • Tutti gli altri vantaggi dell'originale UITextView

3
Se qualcuno sta guardando questa risposta e si sta chiedendo come passare il testo effettivo alla struttura TextView, aggiungi la seguente riga sotto quella che imposta textColor: $ 0.text = "Some text"
Mattl

1
Come leghi il testo a una variabile? Oppure recuperare il testo?
biomiker

1
La prima opzione ha già la rilegatura del testo. Il secondo è uno standard UITextView. Puoi interagire con esso come fai di solito in UIKit.
Mojtaba Hosseini

13

Attualmente, la soluzione migliore è utilizzare questo pacchetto che ho creato chiamato TextView .

Puoi installarlo utilizzando Swift Package Manager (spiegato nel README). Consente lo stato di modifica attivabile e numerose personalizzazioni (anche dettagliate nel README).

Ecco un esempio:

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

In questo esempio, definisci prima due @Statevariabili. Uno è per il testo, su cui scrive TextView ogni volta che viene digitato, e un altro è per lo isEditingstato di TextView.

Il TextView, quando selezionato, alterna lo isEditingstato. Quando si fa clic sul pulsante, si attiva anche lo isEditingstato che mostrerà la tastiera e selezionerà TextView quando truee deselezionerà TextView quando false.


1
Aggiungerò un problema sul repo, ma ha un problema simile alla soluzione originale di Asperi, funziona alla grande in un VStack, ma non in uno ScrollView.
RogerTheShrubber

No such module 'TextView'
Alex Bartiş

Modifica: stai prendendo di mira macOS ma il framework supporta solo UIKit a causa di UIViewRepresentable
Alex Bartiş

11

La risposta di @Meo Flute è fantastica! Ma non funziona per l'immissione di testo in più fasi. E in combinazione con la risposta di @ Asperi, ecco il fisso per quello e ho anche aggiunto il supporto per segnaposto solo per divertimento!

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

Usalo in questo modo:

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

Mi piace questo. Il segnaposto sembra non funzionare ma è stato utile iniziare. Suggerisco di utilizzare colori semantici come UIColor.tertiaryLabel invece di UIColor.lightGray e UIColor.label invece di UIColor.black in modo che siano supportate sia la modalità chiara che quella scura.
Helam

@Helam Ti dispiace spiegare come il segnaposto non funziona?
Daniel Tseng,

@DanielTseng non si presenta. Come dovrebbe comportarsi? Mi aspettavo che mostrasse se il testo è vuoto ma non viene mai visualizzato per me.
Helam

@Helam, nel mio esempio, ho il segnaposto vuoto. Hai provato a cambiarlo in qualcos'altro? ("Hello World!" Invece di "")
Daniel Tseng

Sì, nel mio l'ho impostato come qualcos'altro.
Helam

3

SwiftUI TextView (UIViewRepresentable) con i seguenti parametri disponibili: fontStyle, isEditable, backgroundColor, borderColor e border Width

TextView (text: 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
    }
}

}


Come aggiungerei un testo predefinito al campo di testo multilinea?
Learn2Code

3

Disponibile per Xcode 12 e iOS14 , è davvero semplice.

import SwiftUI

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

non è questo solo se stai lavorando con iOS14, e se l'utente è ancora su iOS13
Di Nerd

3

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

e come usare

struct ContentView: View {

    @State var someString = ""

    var body: some View {
         MultilineTextField(text: $someString)
    }
}

0

Puoi semplicemente usare TextEditor(text: $text)e quindi aggiungere eventuali modificatori per cose come l'altezza.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.