Ich habe versucht, ein mehrzeiliges TextField in SwiftUI zu erstellen, kann aber nicht herausfinden, wie.
Dies ist der Code, den ich derzeit habe:
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
Aber das ist die Ausgabe:
Antworten:
Update: Während Xcode11 Beta 4 jetzt unterstützt
TextView
, habe ich festgestellt, dass das Umschließen von aUITextView
immer noch der beste Weg ist, um bearbeitbaren mehrzeiligen Text zum Laufen zu bringen. Hat beispielsweiseTextView
Anzeigefehler, bei denen der Text in der Ansicht nicht richtig angezeigt wird.Ursprüngliche (Beta 1) Antwort:
Im Moment können Sie
UITextView
a umbrechen, um ein Composable zu erstellenView
: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
quelle
Ok, I started with @sas approach, but needed it really look&feel as multi-line text field with content fit, etc. Here is what I've got. Hope it will be helpful for somebody else... Used Xcode 11.1.
Provided custom MultilineTextField has:
1. content fit
2. autofocus
3. placeholder
4. on commit
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
quelle
backgroundColor
of the UITextField toUIColor.clear
to enable custom backgrounds using SwiftUI and about removing the auto-firstresponder, because it breaks when using multipleMultilineTextFields
in one view (every keystroke, all the text fields try to get the responder again).textField.textContainerInset = UIEdgeInsets.zero
+textField.textContainer.lineFragmentPadding = 0
and it works fine 👌🏻 @Asperi If you do as mentioned, you will then need to remove.padding(.leading, 4)
and.padding(.top, 8)
otherwise it'll look broken. Also, you could change.foregroundColor(.gray)
for.foregroundColor(Color(UIColor.tertiaryLabel))
to match the placeholders' color inTextField
s (I didn't check if if is updating with dark mode).@State private var dynamicHeight: CGFloat = 100
for@State private var dynamicHeight: CGFloat = UIFont.systemFontSize
to fix a small "glitch" when theMultilineTextField
appears (it shows big for a short time and then shrinks).uiView.becomeFirstResponder
This wraps UITextView in Xcode Version 11.0 beta 6 (still working at 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() } }
quelle
isScrollEnabled
in theTextView
implementation; setting a fixed width on the TextView frame; and even putting the TextView and the Text in a ZStack (in the hope that the row would expand to match the height of the Text view) but nothing works. Does anyone have advice on how to adapt this answer to also work in a List?With a
Text()
you can achieve this using.lineLimit(nil)
, and the documentation suggests this should work forTextField()
too. However, I can confirm this does not currently work as expected.I suspect a bug - would recommend filing a report with Feedback Assistant. I have done this and the ID is FB6124711.
EDIT: Update for iOS 14: use the new
TextEditor
instead.quelle
iOS 14
It is called
TextEditor
struct ContentView: View { @State var text: String = "Multiline \ntext \nis called \nTextEditor" var body: some View { TextEditor(text: $text) } }
Dynamic growing height:
If you want it to grow as you type, embed it with a label like below:
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
Using Native UITextView
you can use the native UITextView right in the SwiftUI code with this struct:
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) } }
Usage
struct ContentView: View { var body: some View { TextView() { $0.textColor = .red // Any other setup you like } } }
Advantages:
UIKit
UITextView
quelle
UITextView
. You can interact with it as you usually do in UIKit.Currently, the best solution is to use this package I created called TextView.
You can install it using Swift Package Manager (explained in the README). It allows for toggle-able editing state, and numerous customizations (also detailed in the README).
Here's an example:
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 that example, you first define two
@State
variables. One is for the text, which the TextView writes to whenever it is typed in, and another is for theisEditing
state of the TextView.The TextView, when selected, toggles the
isEditing
state. When you click the button, that also toggles theisEditing
state which will show the keyboard and select the TextView whentrue
, and deselect the TextView whenfalse
.quelle
No such module 'TextView'
@Meo Flute's answer is great! But it doesn't work for multistage text input. And combined with @Asperi's answer, here is the fixed for that and I also added the support for placeholder just for fun!
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 it like this:
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) } } }
quelle
SwiftUI TextView(UIViewRepresentable) with following parameters available: 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 } }
}
quelle
Available for Xcode 12 and iOS14, it's really easy.
import SwiftUI struct ContentView: View { @State private var text = "Hello world" var body: some View { TextEditor(text: $text) } }
quelle
MacOS implementation
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 } } }
and how to use
struct ContentView: View { @State var someString = "" var body: some View { MultilineTextField(text: $someString) } }
quelle