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:
Respostas:
Atualização: Embora o Xcode11 beta 4 agora ofereça suporte
TextView
, descobri que envolver umUITextView
ainda é a melhor maneira de fazer o texto editável de várias linhas funcionar. Por exemplo,TextView
tem 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
UITextView
para criar um combinávelView
: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
fonte
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
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
fonte
backgroundColor
do UITextField paraUIColor.clear
habilitar fundos personalizados usando SwiftUI e em remover o auto-respondedor de início, porque ele quebra ao usar váriosMultilineTextFields
em uma visualização (a cada pressionamento de tecla, todos os campos de texto tentam obter o respondente novamente).textField.textContainerInset = UIEdgeInsets.zero
+textField.textContainer.lineFragmentPadding = 0
e 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 emTextField
s (não verifiquei se está atualizando com o modo escuro).@State private var dynamicHeight: CGFloat = 100
por@State private var dynamicHeight: CGFloat = UIFont.systemFontSize
para consertar uma pequena "falha" quando oMultilineTextField
aparece (fica grande por um curto período e depois encolhe).uiView.becomeFirstResponder
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
TextEditor
lugar.fonte
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() } }
fonte
isScrollEnabled
naTextView
implementaçã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?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
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:
UIKit
UITextView
fonte
UITextView
. Você pode interagir com ele como costuma fazer no UIKit.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
@State
variá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
isEditing
estado. Quando você clica no botão, isso também alterna oisEditing
estado que mostrará o teclado e selecionará TextView quandotrue
, e desmarcará TextView quandofalse
.fonte
No such module 'TextView'
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) } } }
fonte
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 } }
}
fonte
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) } }
fonte
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) } }
fonte
Você pode simplesmente usar
TextEditor(text: $text)
e adicionar quaisquer modificadores para coisas como altura.fonte