TextField
TextField就相当于UIKit中的UITextField的,单行文本输入框。比如登录用户名、密码等。
简单初始化
TextField提供了两种初始化API,一种是通过titleKey,一种是title。代码如下:
public init(_ titleKey: LocalizedStringKey, text: Binding<String>, onEditingChanged: @escaping (Bool) -> Void = { _ in }, onCommit: @escaping () -> Void = {})
public init<S>(_ title: S, text: Binding<String>, onEditingChanged: @escaping (Bool) -> Void = { _ in }, onCommit: @escaping () -> Void = {}) where S : StringProtocol
public init<S, T>(_ title: S, value: Binding<T>, formatter: Formatter, onEditingChanged: @escaping (Bool) -> Void = { _ in }, onCommit: @escaping () -> Void = {}) where S : StringProtocol
以上一共三个初始化API,但是很多参数提供了默认参数,所以衍生中各种初始化的方法,我们先从最简单的讲起,例如下面的初始化方法。
struct TextFieldViewTestView: View {
@State private var inputMessage = ""
var body: some View {
TextField("inputPlaceHolder", text: $inputMessage) //此处调用的是titlekey的初始化api
}
}
struct TextFieldViewTestView_Previews: PreviewProvider {
static var previews: some View {
TextFieldViewTestView()
}
}
首先说一下title,title其实表示的是textfield的placeholder,titlekey也表示placeholder,但是需要通过多语言key来设置。text表示用户所输入的内容的变量绑定
其实这段代码看似简单,但是它牵扯了很多的问题。
1、我们都知道swift函数分为外部参数名,和内部参数名,如果加上_表示外部参数名省略(具体内容可以学习swift),如果外部参数名省略,则再调用的时候不用指明参数名是什么,然而title和titlekey都是省略外部参数名,那么 TextField("inputPlaceHolder", text: $inputMessage)
这行代码是传的title还是titlekey。
因为LocalizedStringKey和S : StringProtocol对于字符串常量的解析和推导是一致的。
我对此也进行了一些测试,我们如果以自动推导的方式设置字符串常量,则编译器总以titlekey为准,如果该key没有被定义,则以key本身为内容显示。
- 多语言的 key被定义,则显示key对应的内容
- 多语言的 key未被定义,显示key本身
那么你可能会有一个疑问,假如我们定义了key,又想显示key本身应该怎么办。我尝试显式传参,即 TextField(title:"inputPlaceHolder", text: $inputMessage)
,但是编译器,提醒我应该删掉“title:”,😓。但是我们先定义一个变量,让编译先把该变量推导成String类型,再初始化TextField,就能显示字符串本身的内容(即以title的api进行初始化,而非titlekey),代码如下:
let s = "inputPlaceHolder"
TextField(s, text: $inputMessage) //此处调用的是title的初始化api,而非titlekey
当然我们也可以推断出,编译器建议我们始终以titlekey的方式初始化,而非title。因为文案类的代码,最好不要写死,如果将来你要支持多语言,你需要把之前所有的代码都适配,这样显然不是一个很好的设计。
2、如何在项目中支持多语言?如何才能支持LocalizedStringKey的使用?
支持多语言是通过.strings文件支持的。当然首先你要想让项目本身是支持多语言的。
如上图,我们先找到项目配置文件,找到Localization,然后通过,+ 号进行语言添加,当然我们也可以不支持多语言,但是即使不支持多语言,我们也应该通过titlekey和.string文件进行文案配置。
选择好语言,就要进行strings文件的创建,首选选择新建文件,选择strings文件,并将文件名修改成Localizable,如图
创建好文件,打开右侧的文件属性页面,点击Localization按钮
弹出对话框,点击Localize
然后再次查看文件属性页面Localization就显示出项目可支持的语音,然后进行勾选
对应的左边文件树这时候可以显示出Localizable.strings的多语言结构
然后分别按照如图格式编辑多语言的LocalizableKey,格式要按照途中显示 key = value 分号换行的格式。要删掉原有自动生成的注释代码
这时候我们回到项目的预览窗口,可以看到LocalizableKey已经生效,如图。
我们如何切换语言看效果呢,我们可以打开Edit Scheme
选择Run -> Options -> App Language,选择其他语言
重新点击Canvas的Resume按钮,重新加载Canvas页面
新的语言效果已经生成
3、Binding<String>是什么东西?@State又是干什么的?
Binding<String>表示要传入一个绑定过的值。@State是一个装饰器,可以将一个变量修饰成State变量。
要想理解这些东西我们首先要理解SwiftUI的状态和数据流。
苹果官方文档写到,
SwiftUI提供了一种声明式的用户界面设计方法。在构建视图的层次结构时,还可以指示视图
的数据依赖关系。当数据更改时,无论是由于外部事件还是由于用户采取的操作,SwiftUI会
自动更新界面中受影响的部分。因此,框架自动执行视图控制器传统上完成的大部分工作。
从图中,我们可以看到,用户和外部事件产生Action,Action产生状态变化,状态变化通知View更新,View更新反馈给用户。
框架提供了一些工具,比如状态变量和绑定,用于将应用程序的数据连接到用户界面。这些
工具可以帮助你为应用程序中的每一条数据维护一个单一的真实源,部分原因是减少了你编
写的粘合逻辑的数量,根据情况选择适合的工具:
1、通过State修饰器,修饰一个View的值类型的属性来管理临时UI状态
2、使用ObservedObject修饰器连接到符合ObservedObject协议的外部引用模型数据。使用EnvironmentObject修饰器访问存储在环境中的可观察对象。使用StateObject在View中直接实例化可观察对象。
3、使用Binding装饰器共享对事实来源的引用,例如状态或可观察对象。
4、通过把存储值到Environment分发值。
5、使用PreferenceKey从子视图向上传递数据。
6、使用FetchRequest管理与核心数据一起存储的持久数据。
从上面的官方文档,我们可以知道,对于Swift的数据状态传递。苹果提供了很多组件给我们用,State修饰器,Binding修饰器,Environment修饰器,ObservedObject修饰器,StateObject修饰器,EnvironmentObject修饰器,PreferenceKey和FetchRequest。
这里我们主要看State和Binding。其他的内容,我们只需要知道就好。
关于State的描述
SwiftUI管理被State修饰的变量值。当状态值更改时,视图将使其外观无效并重新计算实
体。将状态作为创建视图的依据。
状态实例不是值本身;它是读取和写入值的一种方法。要访问状态的基础值,请使用其变量
名,该变量名返回包装的属性值。
您应该只从View的body内部或从状态属性的调用方访问状态属性。因此,请将状态属性声明
为私有,防止其他对象访问该值。从任何线程状态属性都是安全的。
要将状态属性传递给视图层次结构中的另一个视图,请将变量名与$prefix运算符一起使用。
这将从state属性的projectedValue属性检索其绑定。
这部分内容比较难以理解。
被State修饰的变量被系统统一管理。所以状态属性并不是其真正的值,它只是系统提供的一种访问方式。系统(SwiftUI)把状态的值作为视图创建的依据,当State属性发生变化时,系统将会重新创建视图对象。状态属性应该被定义为私有的。它应该只能在body内部使用。当你需要把状态属性传给其他视图的时候,你需要加$前缀。
所以当你的视图需要根据一个值变化而变化时,你就需要创建一个状态属性,并且要定义为私有的。
关于Binding的描述
使用Binding对数据和View之间创建双向连接。Binding将属性连接到存储在其他地方的真正
源,而不是直接存储数据。例如,在播放和暂停之间切换的按钮可以使用绑定属性包装器创
建到其父视图的属性的绑定。
当Binding修饰一个属性的时候,会进行双向链接。并且Binding修饰的属性进行的是引用传递而非值传递。
我们可以看到Binding和State是有区别的。
State是单向绑定。UI会根据状态属性的变化而变化,而Binding是双向绑定,Binding属性变化,视图也会跟着变化,视图变化的,数据也跟着变化。这就是如果播放、暂停一样,播放器本身是不需要视图的,但是视图和播放器要保持同步。State由系统管理,是值传递。Binding是开发者自己管理,是引用传递。
在实践中,State往往用于页面根据数据变化而变化,而Binding用于视图之间的状态传递。
【重点理解】
回到我们这个例子。我们可以知道,当用户的输入发生变化的时候,我们需要更新视图。
这时候我们的两个视图都需要更新。一个是TextField本身,一个是外部视图(即TextFieldViewTestView)。
我们先说外部视图,我们知道外部视图只能根据@State属性来刷新,否则它不会刷新。所以它首先需要一个@State属性。但是我们知道只有TextField才知道用户输入的变化,所以TextField需要通知外部进行刷新,所以这时候TextField要定义一个@Binding的属性来改变外部的@State变量通知TextField刷新。这也就是说为什么@Binding主要用于视图之间状态传递。因为@State是值传递,所以自身变化只会影响自身,用@State无法实现,子视图的父视图的更新,所以才需要有@Binding。
用户输入文字 -> text变化 -> TextField刷新 -> 系统知道text与inputMessage的绑定关系,重新创建新的inputMessage -> inputMessage变化 -> TextFieldViewTestView变化
键盘类型,keyboardType
其实这个方法是View的方法。但是显然输入框是更需要了解这个方法的作用。
该方法传一个枚举参数,枚举定义如下:
public enum UIKeyboardType : Int {
case `default` = 0 //默认键盘,即当前键盘
case asciiCapable = 1 //ASCII键盘 字母键盘+ascii符号
case numbersAndPunctuation = 2 //符号键盘 数字键盘+符号
case URL = 3 //URL键盘 字母键盘+"."+".com"+"/"
case numberPad = 4 //数字键盘 数字键盘(0-9),九宫格
case phonePad = 5 // 电话键盘 数字键盘+"*"+"#"
case namePhonePad = 6 //姓名键盘 正常的键盘,切换表情
case emailAddress = 7 //邮箱键盘 字母键盘+"@"+"."+空格
case decimalPad = 8 //小数键盘 数字键盘+"."
case twitter = 9 // Twitter键盘 字母键盘+"@"+"#"+空格
case webSearch = 10 //搜索键盘 字母键盘+"."+空格
case asciiCapableNumberPad = 11 // A number pad (0-9) that will always be ASCII digits.
}
代码如下:
TextField("inputPlaceHolder", text: $inputMessage)
.keyboardType(. numberPad)
效果
样式,textFieldStyle
该方法仍然是View的方法,但是显然在TextField应该更加关注。
它需要传入一个遵循TextFieldStyle协议的对象。
SwiftUI提供的遵循TextFieldStyle的struct如下:
public struct DefaultTextFieldStyle : TextFieldStyle {
}
public struct PlainTextFieldStyle : TextFieldStyle {
}
public struct RoundedBorderTextFieldStyle : TextFieldStyle {
}
DefaultTextFieldStyle和PlainTextFieldStyle是一样的传统的样式,RoundedBorderTextFieldStyle回带一个矩形边框
代码如下:
struct TextFieldViewTestView: View {
@State private var inputMessage:String = ""
var body: some View {
VStack {
TextField("inputPlaceHolder", text: $inputMessage)
.textFieldStyle(PlainTextFieldStyle())
TextField("inputPlaceHolder", text: $inputMessage)
.textFieldStyle(DefaultTextFieldStyle())
TextField("inputPlaceHolder", text: $inputMessage)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
}
效果如图
如何自定义Style?
可以遵行TextFieldStyle协议,重写_body方法即可,代码如下:
struct CustomTextFieldStyle: TextFieldStyle {
func _body(configuration: TextField<_Label>) -> some View {
configuration
.padding()
.border(Color.accentColor)
}
}
struct TextFieldViewTestView: View {
@State private var inputMessage = ""
var body: some View {
TextField("inputPlaceHolder",text:$inputMessage)
.textFieldStyle(CustomTextFieldStyle())
}
}
效果如图:
一些基本属性的设置
代码如下:
struct TextFieldViewTestView: View {
@State private var inputMessage:String = ""
var body: some View {
TextField("inputPlaceHolder", text: $inputMessage)
.textFieldStyle(PlainTextFieldStyle())
.font(.title) //字体
.foregroundColor(.blue) //字体颜色
.background(Color.red) //背景色
.frame(height:100) //frame
}
}
效果
textCase,大小写转换
View的方法,需要传一个枚举值,枚举定义如下:
public enum Case {
case uppercase //大写
case lowercase //小写
}
它会对于把文本类,例如label和textfield显示的字符串变成全大小或者小写
代码如下:
TextField("inputPlaceHolder", text: $inputMessage)
.textCase(. uppercase)
效果
textContentType输入建议
View的方法,该方法需要传入一个UITextContentType对象,可用的值如下:
extension UITextContentType {
public static let name: UITextContentType
public static let namePrefix: UITextContentType
public static let givenName: UITextContentType
public static let middleName: UITextContentType
public static let familyName: UITextContentType
public static let nameSuffix: UITextContentType
public static let nickname: UITextContentType
public static let jobTitle: UITextContentType
public static let organizationName: UITextContentType、
public static let location: UITextContentType、
public static let fullStreetAddress: UITextContentType、
public static let streetAddressLine1: UITextContentType、
public static let streetAddressLine2: UITextContentType、
public static let addressCity: UITextContentType、
public static let addressState: UITextContentType、
public static let addressCityAndState: UITextContentType、
public static let sublocality: UITextContentType、
public static let countryName: UITextContentType、
public static let postalCode: UITextContentType、
public static let telephoneNumber: UITextContentType、
public static let emailAddress: UITextContentType、
public static let URL: UITextContentType、
public static let creditCardNumber: UITextContentType、
public static let username: UITextContentType、
public static let password: UITextContentType、
public static let newPassword: UITextContentType、
public static let oneTimeCode: UITextContentType
}
设置不同的值,系统会给相应的输入建议
两个事件的监听
在上述初始化的方法里面,可以传入两个闭包,用于监听两个事件
一个是onEditingChanged,当开始编辑和结束编辑的时候会调用,
该闭包会传一个Bool参数,参数指明系统响应的textField是否是当前textField
一个是onCommit,当用户点击return键的时候的回调
代码如下:
struct TextFieldViewTestView: View {
@State private var inputMessage:String = ""
var body: some View {
TextField("inputPlaceHolder", text: $inputMessage) { change in
print(change)
} onCommit: {
print("Commit")
}
}
}
输入一些内容,点击return键,console输出如下
true
Commit
false
Formatter
我们仔细观察带有Formatter参数的初始化方法,text参数不见了,变成了value。
并且value参数不要求是字符串。
Formatter是干嘛的呢,它是个神奇的工具。因为我们有时候并不需要用户输入的原始内容,
因为程序本质是处理数据,而用户输入有时,并不是直接可用的数据。比如说日期,数字,金钱,性别等等。这时候我们往往需要进行一次转换。
我们知道用户输入的东西只能是字符串,而Formatter可以将用户输入绑定成任意类型对象,这是为啥value不再要求绑定的变量是字符串。
我们只需要提供一个把用户输入(即文本或字符串)转换成可用数据的formatter就行了。
当用户输入的可以正常转换成可用对象类型,则将更新值,否则不更新,并提示输入不合法。
这样我们可以直接以可用数据类型进行绑定。这样使代码,灵活简便。让状态,交互,数据流一气呵成。
系统提供了一些已有的Formatter的子类给我们用,有ByteCountFormatter,DateFormatter, DateComponentsFormatter, DateIntervalFormatter, EnergyFormatter, LengthFormatter, MassFormatter, NumberFormatter, PersonNameComponentsFormatter.
先说下比较常见的DateFormatter。
例如代码:
extension DateFormatter {
public static var yyyyMMdd: DateFormatter {
get {
let df = DateFormatter()
df.dateFormat = "yyyyMMdd"
return df
}
}
}
struct TextFieldViewTestView: View {
@State private var inputDate:Date = Date()
var body: some View {
TextField("inputPlaceHolder", value: $inputDate, formatter: DateFormatter.yyyyMMdd,onCommit: {
print(inputDate)
})
}
}
效果图:
控制台输出:
2020-10-16 16:00:00 +0000
2020-10-17 16:00:00 +0000
2020-10-15 18:34:13.606510+0800 SwiftUICourse[10544:14857124] [SwiftUI] The value “20201018ff” is invalid.
2020-10-17 16:00:00 +0000
如何自定义Formatter,官方其实给了一篇文章参考。
链接如下:
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/DataFormatting/Articles/CreatingACustomFormatter.html#//apple_ref/doc/uid/20000196
其实很简单,只需要重写以下两个方法:
stringForObjectValue:
getObjectValue:forString:errorDescription:
具体代码如下:
class SexFormatter: Formatter {
override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool {
if string == "Male"{
obj?.pointee = true as AnyObject
return true
}
if string == "Female"{
obj?.pointee = false as AnyObject
return true
}
error?.pointee = string + " is invalid" as NSString
return false
}
override func string(for obj: Any?) -> String? {
if obj is Bool {
let bValue = obj as! Bool
if bValue {
return "Male"
} else {
return "Female"
}
} else {
return nil
}
}
}
struct TextFieldViewTestView: View {
@State private var sexValue = false
var body: some View {
TextField("inputPlaceHolder", value: $sexValue, formatter: SexFormatter(),onCommit: {
print(sexValue)
})
}
}
控制台输出:
false
2020-10-15 19:28:15.678071+0800 SwiftUICourse[11028:14908758] [SwiftUI] Femaleff is invalid
false
true
关于键盘的收起
TextField收起键盘主要通过UIResponder.resignFirstResponder来实现,以下代码供参考:
extension View {
func endEditing() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
struct TextFieldViewTestView: View {
@State private var inputMessage = ""
var body: some View {
TextField("inputPlaceHolder",text:$inputMessage)
.onTapGesture {}
.onLongPressGesture(
pressing: {
isPressed in
if isPressed {
self.endEditing()
}
},
perform: {}
)
}
}
此代码可以实现,点击输出框键盘弹出,再次点击键盘收起。