TextField 是一个水平扩张型视图,垂直方向不扩张。
// 第1个参数:是输入框的默认占位字符,会以灰色字样式显示在输入框内
TextField("这里就是输入占位符", text: $textFieldData)
为了获取或设置 TextField
中的文本,需要将其绑定到一个变量上。这个变量被传递到 TextField
的初始化器中。然后需要做的就是改变这个绑定变量的文本来改变 TextField
中的内容,或者读取这个绑定变量的值来查看当前 TextField
中的文字。
//1. 先要设置字符串类型的状态变量
@State private var textFieldData = ""
//2. 用$符号绑定变量
TextField("", text: $textFieldData)
// 3. 根据属性的类型不同,需要区别对待,否则会报错
// 如果属性类型是【字符串】,则用 text
TextField("Amount", text: $checkAmount)
// 如果属性类型是【浮点数】等,则用 value
TextField("Amount", value: $checkAmount)
// 显示数字格式(带科学计数法的)
TextField("Enter your score", value: $score, format: .number)
// 显示百分比格式
TextField("Enter your score", value: $score, format: .percent)
// 显示货币格式
TextField("Amount", value: $checkAmount, format: .currency(code: "USD"))
TextField("Amount", value: $checkAmount, format: .currency(code: "CNY"))
// 显示货币格式(下面是设成 .currency ,后面的链式调用&nil合并是设置货币单位,如果无法获取当地货币,则用USD)
TextField("Amount", value: $checkAmount, format: .currency(code: Locale.current.currency?.identifier ?? "USD"))
<aside>
💡 Locale
是内置的一个巨大结构,负责存储用户的所有区域设置——他们使用什么日历、他们如何分隔数字中的千位数字、他们是否使用公制等等。在我们的例子中,我们询问用户是否有首选的货币代码,如果他们没有,我们将回退到“美元”,所以至少我们有一些东西。
</aside>
创建 TextField
时,可以提供一个它可以沿其生长的轴。即 TextField
一开始是单行文本,随着用户键入它可以扩大变成多行输入框。
struct ContentView: View {
@AppStorage("notes") private var notes = ""
var body: some View {
NavigationStack {
TextField("Enter your text", text: $notes, axis: .vertical)
// 可以通过添加 lineLimit() 修饰符来控制 TextField 可以增长的大小
// 例如,我们可能想说它应该以单行开始,但允许最多增长五行:
.lineLimit(5)
.textFieldStyle(.roundedBorder)
}
}
}
// 传入范围,代表:始终至少有两行高,但最多可达五行
.lineLimit(2...5)
// 使用 reservesSpace 参数,以便视图自动为其可以拥有的最大大小分配足够的空间
// 例如,这将创建一个 TextField ,它保留足够的布局空间来容纳最多五行文本:
.lineLimit(5, reservesSpace: true)
SecureField 是一个水平扩张型视图,垂直方向不扩张。
为了获取或设置 SecureField
中的文本,需要将其绑定到一个变量上。这个变量传递到 SecureField
的初始化器中。然后需要做的就是改变这个绑定变量的文本来改变 SecureField
中的内容。或者读取绑定变量的值来查看 SecureField
中当前的文本。SecureField
的工作原理与常规 TextField
几乎相同,不同之处在于,出于保护隐私的目的,这些字符被屏蔽掉了。
struct ContentView: View {
@State private var password: String = ""
var body: some View {
VStack {
SecureField("Enter a password", text: $password)
Text("You entered: \\(password)")
}
}
}
当输入简短文本时,TextField
非常有用;但对较长文本,可能需要切换到 TextEditor
视图,它能更好地为用户提供大量的工作空间。
TextEditor
实际上比 TextField
更容易,我们不能调整其样式或添加占位符文本,能做的只是将它绑定到一个字符串属性TextEditor
时需要确保它不会超出安全区域,否则打字会很棘手;要将其嵌入 NavigationStack
、 Form
或类似内容中Form
内部改变事物的外观,因此请确保在 Form
内部和外部尝试它们,看看它们有何变化。// 例如:将 TextEditor 与 @AppStorage 组合来创建世界上最简单的笔记应用程序
struct ContentView: View {
@AppStorage("notes") private var notes = ""
var body: some View {
NavigationStack {
TextEditor(text: $notes)
.navigationTitle("Notes")
}
}
}
https://www.appcoda.com/swiftui-textview-uiviewrepresentable/
对于复杂的输入框需求,我们需要通过使用 UIViewRepresentable 包装 UIKit 的 UITextView 类来创建文本视图。
TextEditor
视图中,无法直接通过官方 API 获取输入光标的位置并在该位置插入文本。TextEditor
只能绑定一个 String
,但 无法感知光标的 selectedRange
,也没有像 UITextView
那样的 selectedRange
属性。要在 SwiftUI 中使用 UIKit 视图,可以使用 UIViewRepresentable 协议包装该视图。基本上,只需要在 SwiftUI 中创建一个采用该协议的结构来创建和管理 UIView 对象。以下是 UIKit 视图的自定义包装器的骨架:
struct CustomView: UIViewRepresentable {
// makeUIView 方法:负责创建和初始化视图对象
func makeUIView(context: Context) -> some UIView {
// Return the UIView object
}
// updateUIView 方法:负责更新 UIKit 视图的状态
func updateUIView(_ uiView: some UIView, context: Context) {
// Update the view
}
}
// 在实际实现中,可以用想要包装的 UIKit 视图替换一些 UIView。比如为 UITextView 创建自定义包装器,可以编写如下代码:
struct TextView: UIViewRepresentable {
// 在 makeUIView 方法中,返回一个 UITextView 实例。这就是如何包装 UIKit 视图并使其可用于 SwiftUI 的方法
func makeUIView(context: Context) -> UITextView {
return UITextView()
}
func updateUIView(_ uiView: UITextView, context: Context) {
// Update the view
}
}
// 之后要使用 TextView ,就可以像对待任何 SwiftUI 视图一样对待它,并像这样创建它:
struct ContentView: View {
var body: some View {
TextView()
}
}
要为 UITextView
创建自定义包装器,可以编写如下代码:
import SwiftUI
struct TextView: UIViewRepresentable {
// 接受两个绑定:一个用于文本输入保存,另一个用于字体样式
@Binding var text: String
@Binding var textStyle: UIFont.TextStyle
// 在 makeUIView 方法中,我们用 textStyle 文本样式初始化文本视图,返回标准的 UITextView
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.font = UIFont.preferredFont(forTextStyle: textStyle)
textView.autocapitalizationType = .sentences
textView.isSelectable = true
textView.isUserInteractionEnabled = true
return textView
}
// 每当 SwiftUI 中的状态发生变化时,框架都会自动调用 updateUIView 方法来更新视图的配置
func updateUIView(_ uiView: UITextView, context: Context) {
// 1.当在文本视图中键入内容时,我们在这里更新 UITextView 的文本
uiView.text = text
// 2.如果调用者对文本样式进行了任何更改,则 UITextView 可以刷新为新样式
uiView.font = UIFont.preferredFont(forTextStyle: textStyle)
}
}
现在切换到引用 TextView 的主视图,例如 ContentView.swift
// 1.声明两个状态变量来保存文本输入和文本样式
@State private var message = ""
@State private var textStyle = UIFont.TextStyle.body
// 2.显示文本视图,在 body 中插入以下代码
TextView(text: $message, textStyle: $textStyle)
.padding(.horizontal)
<aside> 💡
目前可以在文本视图中键入内容,它会显示您键入的内容。但如果尝试打印出 message 变量的值,则它是空的。因为我们尚未将存储在 UITextView 中的文本同步回 message 变量。
</aside>
UITextView
有一个名为 UITextViewDelegate
的配套协议,它定义了一组可选方法,您可以使用它们来接收相应 UITextView
对象的编辑更改。具体来说,每当用户在文本视图中键入内容时,都会调用以下方法:
optional func textViewDidChange(_ textView: UITextView)
为了跟踪文本的变化, UITextView
对象应该采用 UITextViewDelegate
协议并实现该方法。前面,我们讨论了 UIViewRepresentable
协议中的几个方法。如果您需要使用 UIKit 中的委托并与 SwiftUI 进行通信,则必须实现 makeCoordinator
方法,并提供 Coordinator
实例。此 Coordinator
会充当 UIView 的委托和 SwiftUI 之间的桥梁。
在 TextView
结构体中,创建一个 Coordinator
类,并实现 makeCoordinator
方法,如下所示:
// makeCoordinator 方法仅返回 Coordinator 的一个实例
func makeCoordinator() -> Coordinator {
Coordinator($text)
}
// Coordinator 类:采用 UITextViewDelegate 协议并实现 textViewDidChange 方法
// 如上所述,每次用户更改文本时都会调用此方法。因此,我们捕获更新的文本并通过更新 text 绑定将其传回 SwiftUI
class Coordinator: NSObject, UITextViewDelegate {
var text: Binding<String>
init(_ text: Binding<String>) {
self.text = text
}
func textViewDidChange(_ textView: UITextView) {
self.text.wrappedValue = textView.text
}
}
现在我们有了一个遵循 UITextViewDelegate
协议的 Coordinator
,最后还需要再做一个修改。在 makeUIView
方法中,插入以下代码行,将协调器分配给文本视图:
textView.delegate = context.coordinator
// TextView.swift
struct TextView: UIViewRepresentable {
@Binding var text: String
@Binding var textStyle: UIFont.TextStyle
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.delegate = context.coordinator
textView.font = UIFont.preferredFont(forTextStyle: textStyle)
textView.autocapitalizationType = .sentences
textView.isSelectable = true
textView.isUserInteractionEnabled = true
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
uiView.font = UIFont.preferredFont(forTextStyle: textStyle)
}
func makeCoordinator() -> Coordinator {
Coordinator($text)
}
class Coordinator: NSObject, UITextViewDelegate {
var text: Binding<String>
init(_ text: Binding<String>) {
self.text = text
}
func textViewDidChange(_ textView: UITextView) {
self.text.wrappedValue = textView.text
}
}
}
// ContentView.swift
struct ContentView: View {
@State private var message = ""
@State private var textStyle = UIFont.TextStyle.body
var body: some View {
ZStack(alignment: .topTrailing) {
TextView(text: $message, textStyle: $textStyle)
.padding(.horizontal)
Button(action: {
self.textStyle = (self.textStyle == .body) ? .title1 : .body
}) {
Image(systemName: "textformat")
.imageScale(.large)
.frame(width: 40, height: 40)
.foregroundColor(.white)
.background(Color.purple)
.clipShape(Circle())
}
.padding()
}
}
}
如何在 SwiftUI 使用@FocusState, @FocusedValue and @FocusedObject
想要隐藏弹出的键盘,需要给 SwiftUI 一些方法来确定复选框当前是否应该有焦点(是否应该接收来自用户的文本输入)。其次需要添加某种按钮来在用户需要时删除该焦点,这反过来又会导致键盘消失。整个过程是通过 .focused
修饰符和 @FocusState
状态参数实现的。
// 解决第1个问题需要属性包装器 @FocusState ,它与常规 @State 属性完全相同,只不过是专门为处理 UI 中的输入焦点而设计的
struct ContentView: View {
@State private var name = ""
//1. 定义焦点状态属性
@FocusState private var nameIsFocused: Bool
var body: some View {
VStack {
TextField("Enter your name", text: $name)
//2. 将此修饰符添加到 TextField 后,当文本字段聚焦时,nameIsFocused 是 true,否则为 false
.focused($nameIsFocused)
// 解决第二个问题,可以人为地增加一个按钮
Button("Submit") {
nameIsFocused = false
}
}
}
}
对于更高级的用途,可以使用 @FocusState
来跟踪可选的枚举 case,以检查确定当前聚集的表单字段。例如,我们可能会显示三个文本字段,要求用户提供各种信息,然后在最终信息到达后提交表单:
struct ContentView: View {
enum Field {
case firstName
case lastName
case emailAddress
}
@State private var firstName = ""
@State private var lastName = ""
@State private var emailAddress = ""
@FocusState private var focusedField: Field?
var body: some View {
VStack {
TextField("Enter your first name", text: $firstName)
.focused($focusedField, equals: .firstName)
.textContentType(.givenName)
.submitLabel(.next)
TextField("Enter your last name", text: $lastName)
.focused($focusedField, equals: .lastName)
.textContentType(.familyName)
.submitLabel(.next)
TextField("Enter your email address", text: $emailAddress)
.focused($focusedField, equals: .emailAddress)
.textContentType(.emailAddress)
.submitLabel(.join)
}
.onSubmit {
switch focusedField {
case .firstName:
focusedField = .lastName
case .lastName:
focusedField = .emailAddress
default:
print("Creating account…")
}
}
}
}
<aside> 💡 重要提示:您不应尝试对两个不同的表单字段使用相同的焦点绑定。
</aside>
macOS 上提供了一个 defaultFocus()
修饰符,让我们在视图显示后立即激活一个视图作为用户输入的第一响应者。遗憾的是,它在 iOS 上不存在,但我们可以使用 onAppear()
来解决这个问题。
// iOS 实现方案
struct ContentView: View {
enum FocusedField {
case firstName, lastName
}
@State private var firstName = ""
@State private var lastName = ""
@FocusState private var focusedField: FocusedField?
var body: some View {
Form {
TextField("First name", text: $firstName)
.focused($focusedField, equals: .firstName)
TextField("Last name", text: $lastName)
.focused($focusedField, equals: .lastName)
}
.onAppear {
focusedField = .firstName
}
}
}
// macOS 实现方案
struct ContentView: View {
enum FocusedField {
case firstName, lastName
}
@State private var firstName = ""
@State private var lastName = ""
@FocusState private var focusedField: FocusedField?
var body: some View {
Form {
TextField("First name", text: $firstName)
.focused($focusedField, equals: .firstName)
TextField("Last name", text: $lastName)
.focused($focusedField, equals: .lastName)
}
.defaultFocus($focusedField, .firstName)
}
}