ios 如何在 SwiftUI 中创建多行 TextField?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/56471973/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How do I create a multiline TextField in SwiftUI?
提问by gabriellanata
I've been trying to create a multiline TextField in SwiftUI, but I can't figure out how.
我一直在尝试在 SwiftUI 中创建一个多行 TextField,但我不知道如何。
This is the code I currently have:
这是我目前拥有的代码:
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
But this is the output:
但这是输出:
采纳答案by sas
Update: While Xcode11 beta 4 now does support TextView
, I've found that wrapping a UITextView
is still be best way to get editable multiline text to work. For instance, TextView
has display glitches where text does not appear properly inside the view.
更新:虽然 Xcode11 beta 4 现在确实支持TextView
,但我发现包装 aUITextView
仍然是使可编辑的多行文本工作的最佳方式。例如,TextView
具有显示故障,其中文本在视图内无法正确显示。
Original (beta 1) answer:
原始(测试版 1)答案:
For now, you could wrap a UITextView
to create a composable View
:
现在,您可以包装 aUITextView
以创建一个可组合的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
回答by Asperi
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.
好的,我从 @sas 方法开始,但需要它真正的外观和感觉作为具有内容适合等的多行文本字段。这是我所拥有的。希望对其他人有帮助......使用Xcode 11.1。
Provided custom MultilineTextField has:
1. content fit
2. autofocus
3. placeholder
4. on commit
提供的自定义 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 = 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()
}
}
self.showingPlaceholder = 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)
}
}
}
.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: \(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
}
}
}
}
)")
test = 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)
}
}
}
} )
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
回答by Meo Flute
This wraps UITextView in Xcode Version 11.0 beta 6 (still working at Xcode 11 GM seed 2):
这将 UITextView 包装在 Xcode 版本 11.0 beta 6 中(仍在 Xcode 11 GM 种子 2 中工作):
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)
}
}
回答by Andrew Ebling
With a Text()
you can achieve this using .lineLimit(nil)
, and the documentation suggests this shouldwork for TextField()
too. However, I can confirm this does not currently work as expected.
使用 aText()
可以实现这一点.lineLimit(nil)
,并且文档表明这也应该适用TextField()
。但是,我可以确认这目前没有按预期工作。
I suspect a bug - would recommend filing a report with Feedback Assistant. I have done this and the ID is FB6124711.
我怀疑有错误 - 建议使用反馈助手提交报告。我已经这样做了,ID 是 FB6124711。
回答by Ken Mueller
Currently, the best solution is to use this package I created called TextView.
目前,最好的解决方案是使用我创建的名为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).
您可以使用 Swift Package Manager(在 README 中解释)安装它。它允许切换编辑状态和大量自定义(也在自述文件中详细说明)。
Here's an example:
下面是一个例子:
struct ContentView: View {
var body: some View {
TextView() {
##代码##.textColor = .red
// Any other setup you like
}
}
}
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 the isEditing
state of the TextView.
在该示例中,您首先定义两个@State
变量。一个用于文本,TextView 在输入时写入文本,另一个用于isEditing
TextView的状态。
The TextView, when selected, toggles the isEditing
state. When you click the button, that also toggles the isEditing
state which will show the keyboard and select the TextView when true
, and deselect the TextView when false
.
TextView 在被选中时会切换isEditing
状态。当您单击按钮时,还会切换isEditing
状态,该状态将显示键盘并在 时选择 TextView true
,并在 时取消选择 TextView false
。
回答by Daniel Tseng
@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!
@Meo Flute 的回答很棒!但它不适用于多级文本输入。并结合@Asperi 的回答,这是修复的问题,我还添加了对占位符的支持,只是为了好玩!
##代码##Use it like this:
像这样使用它:
##代码##回答by Mojtaba Hosseini
Using Native UITextView
使用原生 UITextView
you can use the native UITextView right in the SwiftUI code with this struct:
您可以在 SwiftUI 代码中使用本机 UITextView 使用此结构:
##代码##Usage
用法
##代码##Advantages:
- Tested in for years in UIKit
- Native solution
- Scrollable
- Fully customizable
- All other benefits of the original UITextView
优点: - 经过多年的测试UIKit
- 原生解决方案 - 可滚动 - 完全可定制 - 原始版本的所有其他优点UITextView