SwiftUI学习(7)-TextField

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文件支持的。当然首先你要想让项目本身是支持多语言的。


1602658283471.jpg

如上图,我们先找到项目配置文件,找到Localization,然后通过,+ 号进行语言添加,当然我们也可以不支持多语言,但是即使不支持多语言,我们也应该通过titlekey和.string文件进行文案配置。

选择好语言,就要进行strings文件的创建,首选选择新建文件,选择strings文件,并将文件名修改成Localizable,如图


1602658512297.jpg

创建好文件,打开右侧的文件属性页面,点击Localization按钮


1602658561494.jpg

弹出对话框,点击Localize


1602658606926.jpg

然后再次查看文件属性页面Localization就显示出项目可支持的语音,然后进行勾选


1602658632756.jpg

对应的左边文件树这时候可以显示出Localizable.strings的多语言结构


1602658720666.jpg

然后分别按照如图格式编辑多语言的LocalizableKey,格式要按照途中显示 key = value 分号换行的格式。要删掉原有自动生成的注释代码


1602658731610.jpg
1602658760569.jpg

这时候我们回到项目的预览窗口,可以看到LocalizableKey已经生效,如图。


1602659125020.jpg

我们如何切换语言看效果呢,我们可以打开Edit Scheme


1602659196886.jpg

选择Run -> Options -> App Language,选择其他语言


1602659231385.jpg

重新点击Canvas的Resume按钮,重新加载Canvas页面


1602659247404.jpg

新的语言效果已经生成


1602659261932.jpg

3、Binding<String>是什么东西?@State又是干什么的?
Binding<String>表示要传入一个绑定过的值。@State是一个装饰器,可以将一个变量修饰成State变量。

要想理解这些东西我们首先要理解SwiftUI的状态和数据流。
苹果官方文档写到,

SwiftUI提供了一种声明式的用户界面设计方法。在构建视图的层次结构时,还可以指示视图
的数据依赖关系。当数据更改时,无论是由于外部事件还是由于用户采取的操作,SwiftUI会
自动更新界面中受影响的部分。因此,框架自动执行视图控制器传统上完成的大部分工作。
1602660138673.jpg

从图中,我们可以看到,用户和外部事件产生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)

效果


1602752207427.jpg

样式,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())
        }
    }
}

效果如图


1602753798541.jpg

如何自定义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())
    }
}

效果如图:


1602761685777.jpg

一些基本属性的设置

代码如下:

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
    }
}

效果


1602754433121.jpg

textCase,大小写转换

View的方法,需要传一个枚举值,枚举定义如下:

public enum Case {
        case uppercase //大写
        case lowercase //小写
}

它会对于把文本类,例如label和textfield显示的字符串变成全大小或者小写
代码如下:

TextField("inputPlaceHolder", text: $inputMessage)
            .textCase(. uppercase)

效果


1602754669142.jpg

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)
        })
    }
}

效果图:


1602758098432.jpg

控制台输出:

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: {}
            )
    }
}

此代码可以实现,点击输出框键盘弹出,再次点击键盘收起。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259