[英]How do I create a multiline TextField in SwiftUI?
我一直在嘗試在TextField
中創建一個多行文本字段,但我不知道如何。
這是我目前擁有的代碼:
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
但這是 output:
可以使用新的axis
參數將TextField
配置為垂直擴展。 它還需要lineLimit
修飾符來限制給定范圍內的行:
TextField("Title", text: $text, axis: .vertical)
.lineLimit(5...10)
但是 lineLimit 修飾符現在還支持更高級的行為,例如保留最小空間量並隨着更多內容的添加而擴展,然后在內容超過上限時滾動
它被稱為TextEditor
器
struct ContentView: View {
@State var text: String = "Multiline \ntext \nis called \nTextEditor"
var body: some View {
TextEditor(text: $text)
}
}
如果您希望它在您鍵入時增長,請將其嵌入到ZStack
中,並帶有如下Text
:
您可以在 SwiftUI 代碼中使用本機 UITextView 與此結構:
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
的所有其他好處好的,我從@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
更新:雖然 Xcode11 beta 4 現在確實支持TextView
,但我發現包裝UITextView
仍然是讓可編輯的多行文本工作的最佳方式。 例如, TextView
有顯示故障,其中文本在視圖內無法正確顯示。
原始(測試版 1)答案:
現在,您可以包裝一個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
使用Text()
您可以使用.lineLimit(nil)
來實現這一點,並且文檔表明這也適用於TextField()
。 但是,我可以確認這目前無法按預期工作。
我懷疑一個錯誤 - 建議使用反饋助手提交報告。 我已經這樣做了,ID 是 FB6124711。
編輯:iOS 14 更新:改用新的TextEditor
。
這將 UITextView 包裝在 Xcode 版本 11.0 beta 6 中(仍在 Xcode 11 GM 種子 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()
}
}
@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)
}
}
}
目前,最好的解決方案是使用我創建的這個名為TextView的包。
您可以使用 Swift Package Manager 安裝它(在 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 在輸入時寫入該文本,另一個用於 TextView 的isEditing
狀態。
選擇 TextView 后,將切換isEditing
狀態。 當您單擊按鈕時,這也會切換isEditing
狀態,該狀態將顯示鍵盤並在true
時選擇 TextView,在false
時取消選擇 TextView。
我只是嘗試使用lineLimit()在GM的Xcode版本11.0(11A419c)中使用swiftui創建多行文本字段。 它仍然不起作用。 我不敢相信蘋果還沒有解決這個問題。 多行文本字段在移動應用程序中相當普遍。
SwiftUI 有TextEditor
,它類似於TextField
但提供了包含多行的長格式文本輸入:
var body: some View {
NavigationView{
Form{
Section{
List{
Text(question6)
TextEditor(text: $responseQuestion6).lineLimit(4)
Text(question7)
TextEditor(text: $responseQuestion7).lineLimit(4)
}
}
}
}
}
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()
文本視圖(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)
然后為諸如高度之類的東西添加任何修飾符。
這是我根據 Asperi 的回答得出的結論。 此解決方案不需要計算和傳播 size 。 它使用TextView
本身內部的contentSize
和intrinsicContentSize
:
struct TextView: UIViewRepresentable {
@Binding var text: String
func makeUIView(context: UIViewRepresentableContext<TextView>) -> UITextView {
let textView = UIKitTextView()
textView.delegate = context.coordinator
return textView
}
func updateUIView(_ textView: UITextView, context: UIViewRepresentableContext<TextView>) {
if textView.text != self.text {
textView.text = self.text
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text)
}
final private class UIKitTextView: UITextView {
override var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
// Or use e.g. `min(contentSize.height, 150)` if you want to restrict max height
CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
}
final class Coordinator: NSObject, UITextViewDelegate {
var text: Binding<String>
init(text: Binding<String>) {
self.text = text
}
func textViewDidChange(_ textView: UITextView) {
text.wrappedValue = textView.text
}
}
}
// You can use the .multiLineTextAlignment modifier
TextField("Random text", text: $text)
.multiLineTextAlignment(.leading)
// This aligns the text to the left
// There are more properties beside '.leading', more can be found at the source
只想分享我的UITextView
解決方案減去協調器。 我注意到 SwiftUI 調用UITextView.intrinsicContentSize
時沒有告訴它它應該適合什么寬度。默認情況下, UITextView
假定它具有無限的寬度來布置內容,所以如果它只有一行文本,它將返回適合它所需的大小一條線。
為了解決這個問題,我們可以UITextView
並在視圖的寬度發生變化時使內部大小無效,並在計算內部大小時將寬度考慮在內。
struct TextView: UIViewRepresentable {
var text: String
public init(_ text: String) {
self.text = text
}
public func makeUIView(context: Context) -> UITextView {
let textView = WrappedTextView()
textView.backgroundColor = .clear
textView.isScrollEnabled = false
textView.textContainerInset = .zero
textView.textContainer.lineFragmentPadding = 0
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
textView.setContentHuggingPriority(.defaultHigh, for: .vertical)
return textView
}
public func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
}
}
class WrappedTextView: UITextView {
private var lastWidth: CGFloat = 0
override func layoutSubviews() {
super.layoutSubviews()
if bounds.width != lastWidth {
lastWidth = bounds.width
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
let size = sizeThatFits(
CGSize(width: lastWidth, height: UIView.layoutFittingExpandedSize.height))
return CGSize(width: size.width.rounded(.up), height: size.height.rounded(.up))
}
}
我使用文本編輯器
TextEditor(text: $text)
.multilineTextAlignment(.leading)
.cornerRadius(25)
.font(Font.custom("AvenirNext-Regular", size: 20, relativeTo: .body))
//.autocapitalization(.words)
.disableAutocorrection(true)
.border(Color.gray, width: 3)
.padding([.leading, .bottom, .trailing])
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.