Я пытался создать многострочное текстовое поле в SwiftUI, но не могу понять, как это сделать.
Это код, который у меня сейчас есть:
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
Но вот результат:
Ответы:
Обновление: хотя Xcode11 beta 4 теперь поддерживает
TextView
, я обнаружил, что упаковка по-UITextView
прежнему является лучшим способом заставить работать редактируемый многострочный текст. Например,TextView
есть сбои отображения, когда текст не отображается должным образом внутри представления.Исходный ответ (бета 1):
На данный момент вы можете обернуть a,
UITextView
чтобы создать составной объект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
источник
Хорошо, я начал с подхода @sas, но мне нужно, чтобы он действительно выглядел как многострочное текстовое поле с подходящим содержимым и т. Д. Вот что у меня есть. Надеюсь, это будет полезно для кого-то еще ... Использовал Xcode 11.1.
Предоставляемый пользовательский MultilineTextField имеет:
1. соответствие содержимого
2. автофокус
3. заполнитель
4. фиксацию
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
источник
backgroundColor
UITextField дляUIColor.clear
включения настраиваемых фонов с помощью SwiftUI и об удалении автоответчика, потому что он ломается при использовании несколькихMultilineTextFields
в одном представлении (каждое нажатие клавиши, все текстовые поля пытаются снова получить ответчик).textField.textContainerInset = UIEdgeInsets.zero
+textField.textContainer.lineFragmentPadding = 0
и работает отлично 👌🏻 @Asperi Если вы , как уже упоминалось, вам нужно будет удалить.padding(.leading, 4)
и в.padding(.top, 8)
противном случае он будет выглядеть сломана. Кроме того , вы можете изменить.foregroundColor(.gray)
для ,.foregroundColor(Color(UIColor.tertiaryLabel))
чтобы соответствовать цвету заполнителей вTextField
с (я не проверял , если , если обновления в режиме темного).@State private var dynamicHeight: CGFloat = 100
для@State private var dynamicHeight: CGFloat = UIFont.systemFontSize
исправить небольшой «глюк» , когдаMultilineTextField
появляется (это показывает большое за короткое время , а затем сжимается).uiView.becomeFirstResponder
С помощью
Text()
вы можете добиться этого.lineLimit(nil)
, и в документации предполагается, что это тоже должно работатьTextField()
. Однако я могу подтвердить, что в настоящее время это не работает должным образом.Подозреваю ошибку - рекомендую заполнить отчет с помощью Feedback Assistant. Я сделал это, и ID - FB6124711.
РЕДАКТИРОВАТЬ: Обновление для iOS 14:
TextEditor
вместо этого используйте новое .источник
Это обертывает UITextView в Xcode версии 11.0 beta 6 (все еще работает с 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() } }
источник
isScrollEnabled
вTextView
реализации; установка фиксированной ширины рамки TextView; и даже помещая TextView и Text в ZStack (в надежде, что строка расширится до соответствия высоте текстового представления), но ничего не работает. Есть ли у кого-нибудь совет о том, как адаптировать этот ответ для работы в списке?iOS 14
Это называется
TextEditor
struct ContentView: View { @State var text: String = "Multiline \ntext \nis called \nTextEditor" var body: some View { TextEditor(text: $text) } }
Высота динамического роста:
Если вы хотите, чтобы он увеличивался по мере набора текста, добавьте к нему метку, как показано ниже:
ZStack { TextEditor(text: $text) Text(text).opacity(0).padding(.all, 8) // <- This will solve the issue if it is in the same ZStack }
Демо
iOS 13
Использование собственного UITextView
вы можете использовать собственный UITextView прямо в коде SwiftUI с этой структурой:
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) } }
Применение
struct ContentView: View { var body: some View { TextView() { $0.textColor = .red // Any other setup you like } } }
Преимущества:
UIKit
UITextView
источник
UITextView
. Вы можете взаимодействовать с ним, как обычно в UIKit.На данный момент лучшим решением является использование созданного мной пакета под названием TextView .
Вы можете установить его с помощью Swift Package Manager (объяснено в README). Он позволяет переключать состояние редактирования и многочисленные настройки (также подробно описанные в README).
Вот пример:
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) } } }
В этом примере вы сначала определяете две
@State
переменные. Один предназначен для текста, который TextView записывает всякий раз, когда он вводится, а другой - дляisEditing
состояния TextView.TextView, если он выбран, переключает
isEditing
состояние. Когда вы нажимаете кнопку, это также переключаетisEditing
состояние, которое будет отображать клавиатуру и выбрать TextView, когдаtrue
, и отменить выбор TextView, когдаfalse
.источник
No such module 'TextView'
Ответ @Meo Flute отличный! Но для многоступенчатого ввода текста это не работает. И в сочетании с ответом @ Asperi, вот исправление для этого, и я также добавил поддержку заполнителя просто для удовольствия!
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 } } } }
Используйте это так:
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) } } }
источник
SwiftUI TextView (UIViewRepresentable) со следующими доступными параметрами: fontStyle, isEditable, backgroundColor, borderColor и ширина границы.
TextView (текст: 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 } }
}
источник
Доступно для Xcode 12 и iOS14 , это действительно просто.
import SwiftUI struct ContentView: View { @State private var text = "Hello world" var body: some View { TextEditor(text: $text) } }
источник
Реализация 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 } } }
и как использовать
struct ContentView: View { @State var someString = "" var body: some View { MultilineTextField(text: $someString) } }
источник
Вы можете просто использовать,
TextEditor(text: $text)
а затем добавить любые модификаторы для таких вещей, как высота.источник