SwiftUI如何修改页面状态?@state的使用

在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,这样的应用场景应该还是蛮多的。

欢迎关注公众号:iOS进化论,关注后回复“干货”,有福利哦!
SwiftUI如何修改页面状态?@state的使用_第1张图片

你可能感兴趣的:(swiftUI)