TextField
TextField 即为输入框,对应 UIKit 中的 UITextField。
struct ContentView: View { var body: some View { //第一个参数为placeholder,text为输入的内容,textFieldStyle为边框样式 TextField("写点什么进来吧", text: .constant("Hello")) .textFieldStyle(RoundedBorderTextFieldStyle()) //圆角边框 .padding() } }
和 UIKit不同,SwiftUI是一个数据驱动的框架,故输入框输入的内容类型不再是 String,而是 Binding<String>,方便对输入内容的绑定操作。上面的例子暂时用了 constant(value: String)直接将一个字符串转成了Binding<String>。
SecureField
在 UIKit 中,我们只需要设置 UITextField 的 secureTextEntry 属性为 true 即可实现一个密码框,而在 SwiftUI 中,密码框变成了一个独立的 View 为 SecureField,使用方式与 TextField 基本相同。
struct ContentView: View { var body: some View { //第一个参数为placeholder SecureField("请输入密码", text: .constant("")) .textFieldStyle(RoundedBorderTextFieldStyle()) //圆角边框 .padding() } }
Button
Button 即按钮,对应 UIKit 中的 UIButton。
struct ContentView: View { var body: some View { Button("点击") { //点击事件 print("成功点击") } } }
这里使用了 Swift的尾随闭包来绑定点击按钮的事件。
如果我们要自定义这个按钮的具体外观,我们就需要实现 Button 构造函数中的 label 闭包。
struct ContentView: View { var body: some View { Button(action: { print("666") }) { //label闭包区域,设置按钮的样式 Image(systemName: "clock") .renderingMode(.original) //绘制原图 .resizable() .frame(width: 60, height: 60) } } }
label闭包中不单单可以放一个控件,而是可以放多个控件和 Stack 容器进行布局。
实战:简易登录界面的构建
一个登录页面一般有四个元素:头像图片、输入用户名密码的两个输入框、登录按钮。在这里我们让他们纵向排列。设置当账号为 12345、密码为 6 位数字时登录成功,此时改变背景色为红色。
struct ContentView: View { //使用@State修饰两个字符串,使得能用$符号访问后变成Binding<String>绑定在下面的text中 @State var username: String = "" @State var password: String = "" var body: some View { VStack { Image("profilephoto") .resizable() .frame(width: 80, height: 80) .clipShape(Circle()) TextField("请输入用户名", text: $username, onEditingChanged: { isEditing in // 正在编辑时触发 print("正在编辑\(isEditing)") }, onCommit: { //编辑完成后触发 print(self.username) }) .textFieldStyle(RoundedBorderTextFieldStyle()) .padding() SecureField("请输入密码", text: $password, onCommit: { //编辑完成后触发 print(self.password) }) .textFieldStyle(RoundedBorderTextFieldStyle()) .padding() Button("登录"){ //如果密码正确,改变背景色为红色 if self.username == "12345" && self.password.count == 6 { //直接修改rootViewController UIApplication.shared.windows.first?.rootViewController = UIHostingController(rootView: Color.red) } } } } }
在这个例子中我们需要注意,我们要通过属性包装类型 @State 来使得 username 和 password 字符串变成 Binding<String>,用于和两个输入框的输入内容绑定,从而实时获取输入框中的值。
Toggle
Toggle 为开关控件,对应 UIKit 中的 UISwitch。与 UIKit 不同,Toggle 需要绑定一个 Bool 类型的变量来控制开关的状态。
struct ContentView: View { @State var isOpen = true var body: some View { Toggle(isOn: $isOpen) { self.isOpen ? Text("开") : Text("关") //设置开关的标识文字随开关的状态而变化 } .padding() } }
若我们不需要在开关状态变化时改变标识文字,我们可以简写。
Toggle("开关", isOn: $isShowing).padding()
若我们要使得开关能够一直保持打开或者关闭的状态,我们可以绑定一个常量,但这样开关就不能改变状态了。
Toggle("开关", isOn: .constant(true)).padding()
若我们需要更改开关的颜色,可以使用 toggleStyle 修饰符。
struct ContentView: View { @State var isOpen = true var body: some View { Toggle("开关", isOn: $isOpen) .toggleStyle(SwitchToggleStyle(tint: .red)) } }
Slider
Slider 即滑块控件,对应 UIKit 中的 UISlider。我们要将其绑定到一个 Double 类型的变量来获取当前滑块的值。
struct ContentView: View { @State var value: Double = 0.5 //绑定滑块 var body: some View { VStack { //in为滑块值的范围,step为步长 Slider(value: $value, in: 0...1, step: 0.1) { (bool) in print(self.value) } Text("当前滑块的值为\(self.value)") }.padding() } }
我们可以通过 VStack 和 HStack 容器的组合,实现一个亮度调节的界面。
struct ContentView: View { @State var value: Double = 0.5 //绑定滑块 var body: some View { VStack { HStack{ Image(systemName: "sun.min") Slider(value: $value, in: 0...1, step: 0.1) { (bool) in print(self.value) } Image(systemName: "sun.max.fill") }.padding() Text("当前亮度为\(self.value)") } } }
Stepper
Stepper 即步进控制器,对应 UIKit 中的 UIStepper。Stepper 的构造函数也和 Slider 十分类似。
struct ContentView: View { @State var stepValue: Int = 0 var body: some View { Stepper(value: $stepValue, in: 0...10, step: 1, label: { Text("步进控制器的值为: \(self.stepValue)") }) //这里的label同样可以写成尾随闭包的形式,这里将其完整的形式写了出来 } }
若是纯文本的步进控制器,我们同样可以简写。
Stepper("步进控制器: \(self.stepValue)", value: $stepValue, in: 0...10)
如果我们想自定义点按步进控制器增加/减少按钮时执行的动作,可以实现 Stepper 构造函数中 onIncrement 和 onDecrement 两个闭包。
struct ContentView: View { @State var stepValue: Int = 0 var body: some View { Stepper(onIncrement: { self.stepValue += 5 }, onDecrement: { self.stepValue -= 3 }, label: { Text("步进控制器的值为: \(self.stepValue)") }) } }
ForEach
ForEach 用于遍历一个数组或一个区间,从而创建多个 View。它为每个遍历到的数组或区间运行闭包,把当前数据项作为参数传入闭包。
struct ContentView: View { var body: some View { ForEach(0 ..< 20) { number in Text("\(number)") } } }
如果遍历的是区间,则只能是左闭右开区间( ..< )。
如果遍历的是数组,元素必须遵守 Identifiable 协议,假设不符合应该使用 id: \.self 作为第二个参数,因为使用 \.self 会生成传进 ForEach 中的那个数组对象(而不是传统意义下的 ContentView 实例化的对象)的散列值,并据此来唯一地标识数组内的每个元素。
struct ContentView: View { let stringArray = ["abc", "bcd", "cde", "def"] var body: some View { ForEach(stringArray, id: \.self) { item in Text("\(item)") } } }
如果遍历的是数组,我们还可以使用如下的形式进行实现。
struct ContentView: View { @State var names = ["ZhangSan", "LiSi", "WangWu"] var body: some View { // offset表示下标 ForEach(Array(names.enumerated()), id: \.offset) { turple in HStack { Text("\(turple.offset)") Text(turple.element) } } } }
如果我们需要遍历的是一个自定义类型的数组,此时有两种处理方式:
1、使得自定义类型遵守 Identifiable 协议,并且加入一个 id 属性用于唯一的表示某个对象。
2、使用“id: \.某个用于唯一判断的属性名”作为第二个参数传入 ForEach。
//第一种实现方式 struct User: Identifiable { var id = UUID() //id定义为Int型也是可以的,但是下面构造的时候必须要指定对象的唯一编号 var name: String } struct ContentView: View { let users = [User(name: "zhangsan"), User(name: "lisi"), User(name: "wangwu")] var body: some View { //这里不用传第二个参数id ForEach(users) { user in Text(user.name) } } }
//第二种实现方式 struct User { var name: String } struct ContentView: View { let users = [User(name: "zhangsan"), User(name: "lisi"), User(name: "wangwu")] var body: some View { //这里必须要传第二个参数id ForEach(users, id: \.name) { user in Text(user.name) } } }
Picker
Picker 即选择器,对应 UIKit 中的 UIPickerView。默认情况下会有一个文字提示当前选择器的功能,当然我们可以选择关闭。在使用 Picker 时,我们需要绑定一个 Int 型变量来记录当前选择的索引值。
struct ContentView: View { @State var index: Int = 0 //picker的索引 let dataSource = ["红", "橙", "黄", "绿", "青", "蓝", "紫"] var body: some View { VStack{ Picker(selection: $index, label: Text("选择颜色")) { ForEach(0..<dataSource.count) { Text(self.dataSource[$0]) } } Text(dataSource[index]) //显示选中的结果 } } }
UIKit 中的 UISegmentControl,在 SwiftUI 中是一个特殊的 Picker,只需设置 pickerStyle 修饰符即可。
struct ContentView: View { var items = ["红","黄","紫"] @State var currentIndex: Int = 0 var body: some View { VStack{ Picker("颜色", selection: $currentIndex) { ForEach(0 ..< self.items.count) { index in Text(self.items[index]) .tag(index) } } .pickerStyle(SegmentedPickerStyle()) //设置picker的样式,以实现先前的UISegmentControl .padding() Text(items[currentIndex]) } } }
labelsHidden()
用于隐藏 Picker、Stepper、Toggle 等标签文本,如果希望隐藏所有标签,可以将此修饰符应用在最外层容器。
struct ContentView: View { @State var index: Int = 0 let dataSource = ["红", "橙", "黄", "绿", "青", "蓝", "紫"] var body: some View { VStack{ Picker(selection: $index, label: Text("选择颜色")) { ForEach(0..<dataSource.count) { Text(self.dataSource[$0]) } } .labelsHidden() //隐藏文本 Text(dataSource[index]) } } }
将View存储为属性
可以将 View 及其 Modifier 提取出来作为属性使用。
struct ContentView: View { let title = Text("20200721") .font(.largeTitle) let subtitle = Text("星期二") .foregroundColor(.secondary) var body: some View { VStack { //调用前面定义的属性 title .foregroundColor(.red) subtitle } } }