在SwiftUI开发中流传一种说法:视图是状态的函数。这句话什么意思呢?
我们在玩游戏的时候,死了几次,得到几分,收集了一些道具,或者捡到武器,在应用程序中,我们把这些称为state。当你退出游戏的时候状态都会保存,当你下次再进来游戏的时候可以接着上次的继续玩。不过在你玩的过程中,这些所有的东西都可以叫状态:所有的integers,strings,booleans等等,这些存储在内存中的值描述着你现在在干嘛。
回到开头的疑问,我们说SwiftUI的视图们是状态的函数,其实意思是说,页面呈现给我们的样子都由程序的状态来决定,不管是能看到的,还是可以进行交互的内容。比如只有在输入框中填入了内容,我们才能继续进行下一步。而在传统的观念中:用户界面是由一系列事件决定的。我们现在能看到当前页面,是因为我们使用app过程中,点击了某些地方,或者登录了应用,刷新了数据等等。
说了这么多,那在SwiftUI中我们应该如何处理状态呢?我们先以一个按钮的例子开始:
struct ContentView: View {
var tapCount = 0
var body: some View {
Button("Tap Count: \(tapCount)") {
self.tapCount += 1
}
}
}
假如有上面的一个按钮,它有一个title,以及一个action,在按钮点击的时候改变属性tapCount的值。是不是看起来很合理?
实际上上面的代码在swift中是会报错的。ContentView是一个struct,而我们知道结构体是不可变的,所以我们不能随意改变它的值。
在创建一个结构体的方法之后,如果想在方法中修改属性的值,我们需要添加mutating关键字,比如这样:
mutating func doSomeThing()
但是Swift不允许声明可修改的计算型属性,也就是说我们不能这样写:
mutating var body: some View
我们好像陷入了一个僵局:程序运行过程中,我们想改变属性的值,但是Swift的struct不允许这样的操作,那该咋办呢?
这个时候就到我们的主角闪亮登场了,Swift提供了一种特殊的解决方案:属性包装器。它们是一些特殊的关键字,我们可以放置在属性前面来给属性提供超能力!在上面存储状态的例子中,我们是可以使用SwiftUI中的@State属性包装器,像这样:
struct ContentView: View {
@State var tapCount = 0
var body: some View {
Button("Tap Count: \(tapCount)") {
self.tapCount += 1
}
}
}
一点小小的改变,我们的程序就可以成功编译运行了。@State使得我们可以超出struct本身的限制,动态修改属性的值。是不是感觉有点像作弊?哈哈。
有些同学可能就会想到,SwiftUI为什么不直接使用类来表示一个view呢?我们知道类中可以随意的修改属性的值。其实随着学习SwiftUI的深入你会发现,在声明式的SwiftUI中,创建和销毁stuct是很频繁的操作,如果换成类,那会严重影响到程序的性能。
小提示:在使用@State包装一个属性的时候,苹果推荐我们为属性加上private的访问权限,像这样:
@State private var tapCount = 0
@State属性包装器已经允许我们自由修改结构体,这样我们就能在程序发生变化的时候,更新我们的界面了。但上面做的还不够,大家有没有想过,如果一个输入框的输入内容改变了,存储输入内容的属性该如何更新呢?
这就需要讲到双向绑定了。我们希望在页面的UI发生变化的时候,对应的属性也能随之改变,真正做到”页面时状态的函数“。还是拿输入框来说,我们用一个属性绑定了输入框,这样输入框可以展示我们属性中的值,但同时当输入框的内容有任何改变的时候同时也更新我们的属性,这就是双向绑定!
在Swift中我们使用一个特殊符号来表示$
,在属性前加上$
就意味着会读取属性的值同时任何改变也会更新属性的值。比如像这样:
struct ContentView: View {
@State private var name = ""
var body: some View {
Form {
TextField("Enter your name", text: $name)
Text("Hello \(name))
}
}
}
运行后输入你的名字,会看到输入框下面会出现Hello 你的名字,在Text中我们使用name而不是$name
是因为这里是取值,并不需要双向绑定。所以请记住:看到属性前有$
符号,就表明这是一个双向绑定,属性的值读的同时也会被改。
我们还可以给绑定属性添加动画包装,这样在属性值发生变化时页面的变动会有动画效果,比如下面的栗子,开关打开时,TextField会更平滑的出现:
Toggle(isOn: $addLoyaltyDetails.animation()) {
Text("Add iDine loyalty card")
}
if addLoyaltyDetails {
TextField("Enter your iDine ID", text: $loyaltyNumber)
}
SwiftUI允许我们使用Binding类型来自定义双向绑定,我们可以实现自定义的set和get实现。比如下面的代码:
struct ContentView: View {
@State private var username = ""
var body: some View {
let binding = Binding(
get: { self.username },
set: { self.username = $0 }
)
return VStack {
TextField("Enter your name", text: binding)
}
}
}
我们自定义了一个binding,并提供了自己的set和get,在使用的地方,可以看到在binding前面不需要添加$
符号。
那自定义双向绑定的好处是什么呢?我们可以在自定义绑定的set和get中添加自己的逻辑,比如对数据的处理,或者先进行额外的操作,再进行set和get,这样的应用场景应该还是蛮多的。