Próbowałem utworzyć wielowierszowe pole tekstowe w SwiftUI, ale nie wiem, jak to zrobić.
Oto kod, który obecnie posiadam:
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
Ale to jest wynik:
Odpowiedzi:
Aktualizacja: Chociaż Xcode11 beta 4 teraz obsługuje
TextView
, odkryłem, że zawijanie aUITextView
jest nadal najlepszym sposobem na uruchomienie edytowalnego tekstu wielowierszowego. Na przykładTextView
ma błędy wyświetlania, w których tekst nie jest prawidłowo wyświetlany w widoku.Oryginalna (beta 1) odpowiedź:
Na razie możesz opakować a,
UITextView
aby utworzyć kompozycję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
źródło
Ok, zacząłem od podejścia @sas, ale potrzebowałem, żeby naprawdę wyglądało i działało jak wielowierszowe pole tekstowe z dopasowaną zawartością itp. Oto, co mam. Mam nadzieję, że będzie to pomocne dla kogoś innego ... Użyłem Xcode 11.1.
Pod warunkiem, że niestandardowe MultilineTextField ma:
1. dopasowanie treści
2. autofokus
3. symbol zastępczy
4. po zatwierdzeniu
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
źródło
backgroundColor
UITextField, abyUIColor.clear
włączyć niestandardowe tła za pomocą SwiftUI i o usunięciu auto-pierwszej korespondencji, ponieważ psuje się, gdy używasz wieluMultilineTextFields
w jednym widoku (każde naciśnięcie klawisza, wszystkie pola tekstowe próbują ponownie uzyskać odpowiedź).textField.textContainerInset = UIEdgeInsets.zero
+textField.textContainer.lineFragmentPadding = 0
i działa dobrze 👌🏻 @Asperi Jeśli tak jak wspomniano, będziesz musiał usunąć.padding(.leading, 4)
i.padding(.top, 8)
inaczej będzie wyglądać złamany. Możesz także zmienić.foregroundColor(.gray)
na,.foregroundColor(Color(UIColor.tertiaryLabel))
aby dopasować kolor symboli zastępczych wTextField
s (nie sprawdzałem, czy aktualizuje się w trybie ciemnym).@State private var dynamicHeight: CGFloat = 100
za@State private var dynamicHeight: CGFloat = UIFont.systemFontSize
naprawić mały „usterki”, gdyMultilineTextField
pojawi się (to pokazuje duży na krótki czas, a następnie kurczy się).uiView.becomeFirstResponder
To otacza UITextView w Xcode w wersji 11.0 beta 6 (nadal działa na 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() } }
źródło
isScrollEnabled
wTextView
implementacji; ustawienie stałej szerokości ramki TextView; a nawet umieszczenie TextView i Text w ZStack (w nadziei, że wiersz rozszerzy się, aby dopasować wysokość widoku Text), ale nic nie działa. Czy ktoś ma radę, jak dostosować tę odpowiedź, aby działała również na liście?Dzięki a
Text()
możesz to osiągnąć za pomocą.lineLimit(nil)
, a dokumentacja sugeruje, że powinno to działaćTextField()
również. Mogę jednak potwierdzić, że obecnie nie działa to zgodnie z oczekiwaniami.Podejrzewam błąd - poleciłbym zgłoszenie za pomocą Asystenta opinii. Zrobiłem to i numer identyfikacyjny to FB6124711.
EDYCJA: aktualizacja dla iOS 14:
TextEditor
zamiast tego użyj nowego .źródło
iOS 14
To się nazywa
TextEditor
struct ContentView: View { @State var text: String = "Multiline \ntext \nis called \nTextEditor" var body: some View { TextEditor(text: $text) } }
Dynamiczna wysokość wzrostu:
Jeśli chcesz, aby rosła w miarę pisania, umieść ją z etykietą, jak poniżej:
ZStack { TextEditor(text: $text) Text(text).opacity(0).padding(.all, 8) // <- This will solve the issue if it is in the same ZStack }
Próbny
iOS 13
Korzystanie z natywnego UITextView
możesz użyć natywnego UITextView bezpośrednio w kodzie SwiftUI z tą strukturą:
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) } }
Stosowanie
struct ContentView: View { var body: some View { TextView() { $0.textColor = .red // Any other setup you like } } }
Zalety:
UIKit
UITextView
źródło
UITextView
. Możesz z nim wchodzić w interakcje, jak zwykle w UIKit.Obecnie najlepszym rozwiązaniem jest skorzystanie z utworzonego przeze mnie pakietu o nazwie TextView .
Możesz go zainstalować za pomocą Swift Package Manager (wyjaśnione w README). Pozwala na przełączanie stanu edycji i liczne modyfikacje (również szczegółowo opisane w README).
Oto przykład:
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) } } }
W tym przykładzie najpierw definiujesz dwie
@State
zmienne. Jeden dotyczy tekstu, do którego TextView zapisuje się za każdym razem, gdy jest wpisywany, a drugi dotyczyisEditing
stanu TextView.Po zaznaczeniu TextView przełącza
isEditing
stan. Kliknięcie przyciskuisEditing
powoduje również przełączenie stanu, w którym zostanie wyświetlona klawiatura, i wybranie TextView, kiedytrue
, i odznaczenie TextView, kiedyfalse
.źródło
No such module 'TextView'
Odpowiedź @Meo Flute jest świetna! Ale to nie działa w przypadku wieloetapowego wprowadzania tekstu. W połączeniu z odpowiedzią @ Asperi, oto rozwiązanie tego problemu, a także dodałem obsługę symbolu zastępczego tylko dla zabawy!
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 } } } }
Użyj tego w ten sposób:
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) } } }
źródło
SwiftUI TextView (UIViewRepresentable) z dostępnymi parametrami: 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 } }
}
źródło
Dostępne dla Xcode 12 i iOS14 , to naprawdę łatwe.
import SwiftUI struct ContentView: View { @State private var text = "Hello world" var body: some View { TextEditor(text: $text) } }
źródło
Wdrożenie 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 } } }
i jak używać
struct ContentView: View { @State var someString = "" var body: some View { MultilineTextField(text: $someString) } }
źródło