Como faço para criar um TextField multilinha no SwiftUI?

93

Tenho tentado criar um TextField multilinha no SwiftUI, mas não consigo descobrir como.

Este é o código que tenho atualmente:

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

Mas esta é a saída:

insira a descrição da imagem aqui

gabriellanata
fonte
1
Eu apenas tentei fazer um campo de texto multilinha com swiftui no Xcode versão 11.0 (11A419c), o GM, usando lineLimit (). Ele ainda não funciona. Não acredito que a Apple ainda não corrigiu isso. Um campo de texto multilinha é bastante comum em aplicativos móveis.
e987

Respostas:

49

Atualização: Embora o Xcode11 beta 4 agora ofereça suporte TextView, descobri que envolver um UITextViewainda é a melhor maneira de fazer o texto editável de várias linhas funcionar. Por exemplo, TextViewtem falhas de exibição onde o texto não aparece corretamente dentro da visualização.

Resposta original (beta 1):

Por enquanto, você pode embrulhar um UITextViewpara criar um combinável 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

insira a descrição da imagem aqui

sas
fonte
Ótima solução temporária! Aceitando por enquanto, até que possa ser resolvido usando SwiftUI puro.
gabriellanata
7
Esta solução permite que você exiba texto que já contém novas linhas, mas não parece quebrar / quebrar linhas naturalmente longas. (O texto continua crescendo horizontalmente em uma linha, fora do quadro.) Alguma ideia de como fazer linhas longas quebrar?
Michael
6
Se eu usar o State (em vez de um EnvironmentObject com um Publisher) e passá-lo como uma ligação para MultilineTextView, ele não parece funcionar. Como posso refletir as alterações de volta para o estado?
cinza
Existe alguma maneira de definir um texto padrão na textview sem usar um environmentObject?
Learn2Code
83

Ok, comecei com a abordagem @sas, mas precisava realmente parecer e funcionar como um campo de texto de várias linhas com ajuste de conteúdo, etc. Aqui está o que eu tenho. Espero que seja útil para outra pessoa ... Xcode 11.1 usado.

O MultilineTextField personalizado fornecido tem:
1. ajuste de conteúdo
2. foco automático
3. marcador de posição
4. na confirmação

Visualização do campo de texto de várias linhas do swiftui com ajuste de conteúdo Marcador de posição adicionado

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
fonte
6
Além disso, você deve pensar em configurar o backgroundColordo UITextField para UIColor.clearhabilitar fundos personalizados usando SwiftUI e em remover o auto-respondedor de início, porque ele quebra ao usar vários MultilineTextFieldsem uma visualização (a cada pressionamento de tecla, todos os campos de texto tentam obter o respondente novamente).
iComputerfreak
2
@ kdion4891 Conforme explicado nesta resposta a partir de outra pergunta , você pode simplesmente fazer textField.textContainerInset = UIEdgeInsets.zero+ textField.textContainer.lineFragmentPadding = 0e funciona bem 👌🏻 @Asperi Se você fizer conforme mencionado, precisará removê .padding(.leading, 4)- .padding(.top, 8)lo; caso contrário, ele parecerá quebrado. Além disso, você pode alterar .foregroundColor(.gray)para .foregroundColor(Color(UIColor.tertiaryLabel))para corresponder à cor dos espaços reservados em TextFields (não verifiquei se está atualizando com o modo escuro).
Rémi B.
3
Ah, e, também troquei @State private var dynamicHeight: CGFloat = 100por @State private var dynamicHeight: CGFloat = UIFont.systemFontSizepara consertar uma pequena "falha" quando o MultilineTextFieldaparece (fica grande por um curto período e depois encolhe).
Rémi B.
2
@ q8yas, você pode comentar ou remover o código relacionado auiView.becomeFirstResponder
Asperi
3
Obrigado a todos pelos comentários! Eu realmente aprecio isso. O instantâneo fornecido é uma demonstração da abordagem, que foi configurada para um propósito específico. Todas as suas propostas estão corretas, mas para seus propósitos. Você está livre para copiar e colar este código e reconfigurá-lo tanto quanto desejar para sua finalidade.
Asperi
31

Com um, Text()você pode conseguir isso usando .lineLimit(nil), e a documentação sugere que isso deve funcionar paraTextField() . No entanto, posso confirmar que atualmente isso não funciona conforme o esperado.

Eu suspeito de um bug - recomendo preencher um relatório com o Assistente de Feedback. Eu fiz isso e o ID é FB6124711.

EDIT: Atualização para iOS 14: use o novo em seu TextEditorlugar.

Andrew Ebling
fonte
Existe uma maneira de pesquisar o bug usando o id FB6124711? Como estou verificando o assistente de feedback, mas não é muito útil
CrazyPro007
Não acredito que haja uma maneira de fazer isso. Mas você poderia mencionar esse ID em seu relatório, explicando que o seu é um idiota do mesmo problema. Isso ajuda a equipe de triagem a aumentar a prioridade do problema.
Andrew Ebling
2
Confirmado que isso ainda é um problema no Xcode versão 11.0 beta 2 (11M337n)
Andrew Ebling
3
Confirmado que ainda é um problema no Xcode versão 11.0 beta 3 (11M362v). Você pode definir a string como "Algum \ ntexto" e ela será exibida em duas linhas, mas digitar o novo conteúdo fará com que uma linha de texto cresça horizontalmente, fora do quadro de sua visualização.
Michael
3
Isso ainda é um problema no Xcode 11.4 - Sério ??? Como devemos usar o SwiftUI na produção com bugs como este.
Trev14,
29

Isso envolve o UITextView no Xcode versão 11.0 beta 6 (ainda funcionando no 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
fonte
1
TextField ainda não é afetado por lineLimit () no Xcode Versão 11.0 (11A420a), GM Seed 2, setembro de 2019
e987
2
Isso funciona bem em um VStack, mas ao usar um List, a altura da linha não se expande para mostrar todo o texto no TextView. Eu tentei algumas coisas: mudar isScrollEnabledna TextViewimplementação; definir uma largura fixa no quadro TextView; e até mesmo colocar o TextView e o Texto em um ZStack (na esperança de que a linha se expanda para corresponder à altura da visualização do Texto), mas nada funciona. Alguém tem algum conselho sobre como adaptar esta resposta para também funcionar em uma lista?
MathewS
@Meo Flute está lá para fazer a altura corresponder ao conteúdo.
Abdullah
Alterei isScrollEnabled para false e funciona, obrigado.
Abdullah
28

iOS 14

É chamado TextEditor

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

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

Altura de crescimento dinâmico:

Se você quiser que ele cresça conforme você digita, incorpore-o com um rótulo como abaixo:

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

Usando UITextView nativo

você pode usar o UITextView nativo diretamente no código SwiftUI com esta estrutura:

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

Vantagens:

  • Suporte para iOS 13
  • Compartilhado com o código legado
  • Testado por anos em UIKit
  • Totalmente personalizável
  • Todos os outros benefícios do original UITextView
Mojtaba Hosseini
fonte
3
Se alguém estiver olhando para esta resposta e se perguntando como passar o texto real para a estrutura TextView, adicione a seguinte linha abaixo daquela que define textColor: $ 0.text = "Some text"
Mattl
1
Como você vincula o texto a uma variável? Ou recuperar o texto?
biomista
1
A primeira opção já possui a vinculação do texto. O segundo é um padrão UITextView. Você pode interagir com ele como costuma fazer no UIKit.
Mojtaba Hosseini
13

Atualmente, a melhor solução é usar este pacote que criei chamado TextView .

Você pode instalá-lo usando o Swift Package Manager (explicado no README). Ele permite um estado de edição alternável e várias personalizações (também detalhadas no README).

Aqui está um exemplo:

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

Nesse exemplo, você primeiro define duas @Statevariáveis. Um é para o texto, que o TextView grava sempre que é digitado, e outro é para oisEditing estado do TextView.

O TextView, quando selecionado, alterna o isEditingestado. Quando você clica no botão, isso também alterna o isEditingestado que mostrará o teclado e selecionará TextView quando true, e desmarcará TextView quando false.

Ken Mueller
fonte
1
Vou adicionar um problema no repo, mas ele tem um problema semelhante à solução original do Asperi, ele funciona muito bem em um VStack, mas não em um ScrollView.
RogerTheShrubber
No such module 'TextView'
Alex Bartiş
Edit: você está direcionando o macOS, mas a estrutura só oferece suporte ao UIKit por causa do UIViewRepresentable
Alex Bartiş
11

A resposta da @Meo Flute é ótima! Mas não funciona para entrada de texto em vários estágios. E combinado com a resposta de @Asperi, aqui está o conserto para isso e eu também adicionei o suporte para placeholder apenas por diversão!

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

Use-o assim:

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
fonte
Eu gosto disso. O marcador de posição não parece estar funcionando, mas foi útil para começar. Eu sugiro usar cores semânticas como UIColor.tertiaryLabel em vez de UIColor.lightGray e UIColor.label em vez de UIColor.black para que os modos claro e escuro sejam suportados.
Helam
@Helam Você se importa de explicar como o placeholder não está funcionando?
Daniel Tseng
@DanielTseng não aparece. Como deve se comportar? Eu esperava que ele mostrasse se o texto está vazio, mas nunca mostra para mim.
Helam
@Helam, no meu exemplo, tenho o espaço reservado para estar vazio. Você já tentou alterá-lo para outra coisa? ("Olá, mundo!" Em vez de "")
Daniel Tseng,
Sim, no meu, eu o defini como outra coisa.
Helam
3

SwiftUI TextView (UIViewRepresentable) com os seguintes parâmetros disponíveis: fontStyle, isEditable, backgroundColor, borderColor & 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
    }
}

}

Di Nerd
fonte
Como eu adicionaria um texto padrão ao campo de texto de várias linhas?
Learn2Code
3

Disponível para Xcode 12 e iOS14 , é muito fácil.

import SwiftUI

struct ContentView: View {
    
    @State private var text = "Hello world"
    
    var body: some View {
        TextEditor(text: $text)
    }
}
gandhi Mena
fonte
Isso não é apenas se você estiver trabalhando com iOS14, e se o usuário ainda estiver no iOS13
Di Nerd
3

Implementação 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 como usar

struct ContentView: View {

    @State var someString = ""

    var body: some View {
         MultilineTextField(text: $someString)
    }
}
Denis Rybkin
fonte
0

Você pode simplesmente usar TextEditor(text: $text)e adicionar quaisquer modificadores para coisas como altura.

JMan
fonte