177 votes

Comment créer un TextField multiligne dans SwiftUI ?

J'ai essayé de créer un multiligne TextField dans SwiftUI, mais je n'arrive pas à trouver comment.

Voici le code que j'ai actuellement :

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

Mais c'est la sortie :

enter image description here

2 votes

Je viens d'essayer de faire un champ texte multiligne avec swiftui dans Xcode Version 11.0 (11A419c), la GM, en utilisant lineLimit(). Cela ne fonctionne toujours pas. Je ne peux pas croire qu'Apple n'ait pas encore corrigé ce problème. Un champ de texte multiligne est assez commun dans les applications mobiles.

211voto

Mojtaba Hosseini Points 2525

IOS 14 - SwiftUI natif

Il s'appelle TextEditor

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

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

Hauteur de croissance dynamique :

Si vous voulez qu'il grandisse au fur et à mesure que vous tapez, intégrez-le dans un fichier de type ZStack avec un Text comme ça :

Demo


iOS 13 - Utilisation de UITextView

vous pouvez utiliser le UITextView natif directement dans le code SwiftUI avec cette structure :

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

Utilisation

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

Avantages :

  • Soutien aux iOS 13
  • Partagé avec l'ancien code
  • Testé pendant des années dans UIKit
  • Entièrement personnalisable
  • Tous les autres avantages de l'original UITextView

126voto

Asperi Points 123157

Ok, j'ai commencé avec l'approche @sas, mais j'avais besoin que cela ressemble vraiment à un champ de texte de plusieurs lignes avec un contenu adapté, etc. Voici ce que j'ai obtenu. J'espère que cela sera utile pour quelqu'un d'autre... J'ai utilisé Xcode 11.1.

Fourni le champ MultilineTextField personnalisé a :
1. adéquation du contenu
2. l'autofocus
3. placeholder
4. sur l'engagement

Preview of swiftui multiline textfield with content fitAdded 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

60voto

sas Points 2391

Mise à jour : Bien que Xcode11 beta 4 supporte maintenant TextView j'ai découvert qu'emballer un UITextView est encore le meilleur moyen de faire fonctionner un texte multiligne modifiable. Par exemple, TextView a des problèmes d'affichage où le texte n'apparaît pas correctement dans la vue.

Réponse originale (bêta 1) :

Pour l'instant, vous pouvez envelopper un UITextView pour créer un 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

37voto

Andrew Ebling Points 1994

Avec un Text() vous pouvez y parvenir en utilisant .lineLimit(nil) et la documentation suggère ceci devrait travailler pour TextField() aussi. Cependant, je peux confirmer que cela ne fonctionne pas actuellement comme prévu.

Je pense qu'il s'agit d'un bogue - je vous recommande d'envoyer un rapport à Feedback Assistant. Je l'ai fait et l'ID est FB6124711.

EDIT : Mise à jour pour iOS 14 : utilisez la nouvelle fonction TextEditor à la place.

0 votes

Existe-t-il un moyen de rechercher le bogue en utilisant l'id FB6124711 ? Je vérifie l'assistant de retour d'information, mais il n'est pas très utile.

0 votes

Je ne crois pas qu'il y ait un moyen de le faire. Mais vous pouvez mentionner cet identifiant dans votre rapport, en expliquant que le vôtre est une copie du même problème. Cela aide l'équipe de triage à augmenter la priorité du problème.

0 votes

Je vois ça aussi. Pour l'avenir, la version de Xcode disponible au moment de ce rapport et de cette réponse est la suivante : Version 11.0 beta (11M336w) [la toute première version beta de Xcode 11]. J'espère que cette réponse (et cette question) deviendra très bientôt obsolète.

32voto

Meo Flute Points 891

Cela enveloppe UITextView dans la version 11.0 beta 6 de Xcode (qui fonctionne toujours avec 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 votes

TextField n'est toujours pas affecté par lineLimit() dans Xcode Version 11.0 (11A420a), GM Seed 2, Sept. 2019

3 votes

Cela fonctionne bien dans une VStack, mais lorsqu'on utilise une liste, la hauteur de la ligne ne s'étend pas pour afficher tout le texte dans le TextView. J'ai essayé plusieurs choses : modifier isScrollEnabled dans le TextView J'ai même placé le TextView et le texte dans une pile Z (dans l'espoir que la ligne s'étende pour correspondre à la hauteur de la vue du texte), mais rien ne fonctionne. Quelqu'un a-t-il des conseils sur la façon d'adapter cette réponse pour qu'elle fonctionne également dans une liste ?

0 votes

@Meo Flute existe-t-il un moyen de faire correspondre la hauteur au contenu ?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X