SwiftUI 学习笔记(三):常见的 View 和 Modifier 解析(二)

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

若我们需要更改开关的颜色,可以使用 onAppear 修饰符。

struct ContentView: View {
    
    @State var isOpen = true
    
    var body: some View {
        Toggle("开关", isOn: $isOpen)
            .onAppear {
                UISwitch.appearance().onTintColor = .orange
        }
    }
}

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

如果你对采用 \.self 作为 id的参数仍有疑惑,可以阅读下面这篇博客:https://www.jianshu.com/p/1665e7473c1e,可能会对你有所启发。

如果遍历的是数组,我们还可以使用如下的形式进行实现。

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

发表评论

电子邮件地址不会被公开。 必填项已用*标注