上一篇博客我们介绍了 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 会自动重新计算依赖于该状态的视图,
从而确保用户界面准确地反映更新后的状态。
只能从视图的 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()
方法来切换布尔值 isPlaying
在 false
和 true
之间。
这允许我们在播放和停止按钮之间切换。在预览画布中,点击播放图标将触发切换并观察按钮外观的相应变化。
实际上,你可能已经注意到 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
视图接受两个参数:counter
和 color
。你可以这样创建一个红色按钮:
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 应用的用户体验。