Skip to main content

SwiftUI 状态管理详解:深入理解 `@State` 和 `@Binding`

鱼雪

上一篇博客我们介绍了 SwiftUI 中的 Button 控件,

今天我们来介绍 SwiftUI 中的 @State@Binding 控件。

在应用开发中,状态管理是每个开发者必须面对的关键问题。 让我们以开发一个音乐播放器应用为例。当用户点击播放按钮时,按钮应切换为停止按钮。 为了实现这一功能,你需要一个机制来跟踪应用的状态,以便在适当的时候改变按钮的外观。

SwiftUI 提供了多种内置的状态管理功能,其中之一就是 @State 属性包装器。 当你使用 @State 注解一个属性时,SwiftUI 会自动将其存储在应用中。 此外,使用该属性的视图会在其值发生变化时自动接收到通知。 因此,当状态发生变化时,SwiftUI 会重新计算受影响的视图并相应地更新应用的外观。

听起来是不是很棒?不过,我理解状态管理一开始可能有些让人困惑。 为了更好地理解状态(@State)绑定(@Binding), 本文将通过编码示例和练习来深入讲解这些概念。

通过这些示例,你将更好地掌握 SwiftUI 中这一关键概念。

创建一个启用 SwiftUI 的新项目

首先,让我们使用 SwiftUI 创建一个简单的示例, 演示如何通过跟踪应用状态在播放按钮和停止按钮之间切换。 打开 Xcode,选择 App 模板创建一个新项目。 在后续的屏幕中,将项目名称设置为 SwiftUIState(你也可以选择其他名称), 确保 Interface 选项中选择 SwiftUI

保存项目后,Xcode 会加载 ContentView.swift 文件并在画布中显示预览。 为了创建播放按钮,可以使用以下代码片段:

Button {
// 切换播放和停止按钮
} label: {
Image(systemName: "play.circle.fill")
.font(.system(size: 150))
.foregroundStyle(.green)
}

我们使用了系统图像 play.circle.fill 并将按钮颜色设置为绿色。

控制按钮的状态

当前按钮的操作是空的,但我们希望在按钮被点击时,将其外观从播放按钮切换为停止按钮, 并将按钮颜色更改为红色。那么,我们该如何实现呢? 显然,我们需要一个变量来跟踪按钮的状态。让我们将其命名为 isPlaying。 这是一个布尔变量,用于指示应用是否处于播放状态。

  • 如果 isPlaying 设置为 true,应用应显示停止按钮
  • 如果设置为 false,则显示播放按钮。

以下是实现此行为的代码:

struct ContentView: View {
private var isPlaying = false
var body: some View {
Button {
// 切换播放和停止按钮
} label: {
Image(systemName: isPlaying ? "stop.circle.fill" : "play.circle.fill")
.font(.system(size: 150))
.foregroundStyle(isPlaying ? .red : .green)
}
}
}

我们根据 isPlaying 变量的值修改图像名称和颜色。

  • 如果将 isPlaying 的默认值设置为 false,预览画布中将显示播放按钮
  • 如果isPlaying设置为 true,则显示停止按钮

现在的问题是:应用如何监控状态(即 isPlaying)的变化并自动更新按钮?

  • 在 SwiftUI 中,这一功能的实现非常简单
  • 只需在 isPlaying 属性前加上 @State
@State private var isPlaying = false

通过将 isPlaying 属性声明为状态变量,SwiftUI 会自动管理其存储并监控其值的变化。 每当 isPlaying 的值发生变化时,SwiftUI 会自动重新计算依赖于该状态的视图, 从而确保用户界面准确地反映更新后的状态。

note

只能从视图的 body 内部(或其调用的函数中)访问状态属性。因此,应该将状态属性声明为 private,以防止视图的外部访问。

我们尚未实现按钮的具体操作,现在让我们完成它:

Button {
// 切换播放和停止按钮
isPlaying.toggle()
} label: {
Image(systemName: isPlaying ? "stop.circle.fill" : "play.circle.fill")
.font(.system(size: 150))
.foregroundColor(isPlaying ? .red : .green)
}

action 闭包中,我们调用 toggle() 方法来切换布尔值 isPlayingfalsetrue 之间。 这允许我们在播放和停止按钮之间切换。在预览画布中,点击播放图标将触发切换并观察按钮外观的相应变化。

实际上,你可能已经注意到 SwiftUI 在切换按钮时会呈现淡入淡出的动画。 这种动画是 SwiftUI 内置的,无需额外编写代码即可实现平滑的按钮外观过渡,提供无缝的用户体验。

在本书的后续章节中,我们将深入探讨动画主题,学习如何使用 SwiftUI 自定义和创建自己的动画。 正如你所见,SwiftUI 简化了实现 UI 动画的过程,使其对所有开发者更具可访问性。

使用 Binding 进行状态管理

成功创建计数按钮后,我们将进一步探讨 @Binding 的使用。 在这种情况下,不再声明一个布尔变量作为状态,而是使用一个整数状态变量来跟踪计数。 每当按钮被点击,计数器将增加 1。

接下来,我们修改代码以显示三个计数按钮,如图所示。所有三个按钮将共享同一个计数器。 无论点击哪个按钮,计数器都会增加 1,所有按钮将更新以显示最新的计数值。

为了避免代码重复,良好的实践是将公共视图提取为可重用的子视图。 在本例中,我们可以将 Button 视图提取出来,创建一个独立的子视图。

以下是实现方法的示例:

struct CounterButton: View {
@Binding var counter: Int
var color: Color
var body: some View {
Button {
counter += 1
} label: {
Circle()
.frame(width: 200, height: 200)
.foregroundStyle(color)
.overlay {
Text("\(counter)")
.font(.system(size: 100, weight: .bold, design: .rounded))
.foregroundStyle(.white)
}
}
}
}

CounterButton 视图接受两个参数:countercolor。你可以这样创建一个红色按钮:

CounterButton(counter: $counter, color: .red)

你可能注意到,CounterButton 视图中的 counter 变量使用了 @Binding 注解。 此外,当创建 CounterButton 的实例时,counter 参数前面加上了 $ 符号。

这些符号的含义是什么?

将按钮提取为单独的视图后,CounterButton 成为 ContentView 的子视图。 计数器的递增现在在 CounterButton 视图中进行,而不是在 ContentView 中。 为了在 ContentView 中管理状态变量,CounterButton 需要一种方式来访问和更新它。

通过使用 @Binding$ 符号,CounterButton 视图中对 counter 值的更改将反映在父视图(即 ContentView)中,反之亦然。 这使得在父视图中所有按钮的计数值能够同步更新。

也就是说如果要在父视图中管理状态变量,但是又想在子视图中使用这个状态变量, 那么需要在父视图中使用@State声明为状态变量,在子视图中使用@Binding声明为绑定变量。 并且需要在父视图中使用$符号来传递状态变量的引用。

这样,子视图就可以通过绑定变量来访问和更新父视图中的状态变量。

现在,你已经理解了绑定的工作原理,可以继续创建另外两个按钮并使用 VStack 垂直对齐它们。

以下是实现方法的示例:

struct ContentView: View {
@State private var counter = 1
var body: some View {
VStack {
CounterButton(counter: $counter, color: .blue)
CounterButton(counter: $counter, color: .green)
CounterButton(counter: $counter, color: .red)
}
}
}

完成修改后,你可以在预览画布中测试应用。 点击任意按钮将使计数增加一,所有按钮将更新以反映最新的计数值。

所有小案例的代码

最终的案例是实现三个不同颜色的计数按钮,并且每个按钮的计数是独立的,最后三个按钮的计数相加显示在顶部。

以下是本文中提到的所有代码:

import SwiftUI

struct ContentView: View {
@State private var redCounter = 0
@State private var blueCounter = 0
@State private var greenCounter = 0

var body: some View {
VStack{
Text("\(redCounter + blueCounter + greenCounter)")
.font(.system(size: 150))
.fontWeight(.bold)

HStack{
CounterButton(count: $redCounter, color: .red)
CounterButton(count: $blueCounter, color: .blue)
CounterButton(count: $greenCounter, color: .green)
}

}

}
}

struct CounterButton: View {
@Binding var count: Int

var color: Color

var body: some View {
Button(
action:{
count += 1
},
label: {
Circle()
.frame(width: 100, height: 100)
.foregroundColor(color)
.overlay(
Text("\(count)")
.font(.system(size: 40, weight: .bold, design: .rounded))
.foregroundColor(.white)
)
}
)
}
}

struct Demo3View: View {
@State private var count = 0
var body: some View {
VStack {
CounterButton(count: $count, color: .red)
CounterButton(count: $count, color: .blue)
CounterButton(count: $count, color: .green)
}
}
}

struct Demo2View: View {
@State private var count = 0
var body: some View {
Button(
action: {
count += 1
},
label: {
ZStack {
Image(systemName: "circle.fill")
.font(.system(size: 150))
.foregroundColor(.red)
Text("\(count)")
.font(.system(size: 50))
.foregroundColor(.white)
}
})
}
}

struct Demo1View: View {
@State private var isPlaying = false
var body: some View {
Button(action: {
isPlaying.toggle()
}) {
Image(systemName: isPlaying ? "stop.circle.fill": "play.circle.fill")
.font(.system(size: 100))
.foregroundColor(isPlaying ? .red : .green)
}
}
}

#Preview {
ContentView()
}

总结

SwiftUI 中对状态的支持简化了应用开发中的状态管理。 理解 @State@Binding 的概念至关重要, 因为它们在 SwiftUI 中管理状态和更新用户界面方面发挥着重要作用。

本文介绍了 SwiftUI 中状态管理的基础知识。 随着学习的深入,你将了解如何利用 @State 实现视图动画,以及如何在多个视图之间管理共享状态。

通过结合使用 @State@Binding,SwiftUI 使得构建响应式且高效的用户界面变得更加简便。 希望本文能帮助你在 SwiftUI 中更好地管理状态,提升 iOS 应用的用户体验。