值类型的数据共享机制,是 SwiftUI 中最常用最基本的机制。
SwiftUI 的 @State 属性包装器主要为当前视图的简单数据而设计的。SwiftUI 说 “视图是其状态的函数”,意思是用户界面的外观(人可以看到的内容以及可交互的内容),是由程序的状态决定的。@State 就是一种当前视图的程序状态。
想要理解其产生背景,可以看以下代码,它会报错。因为 ContentView 是一个结构体,可以将其创建为常量的实例,这意味着它是不可变的,我们不能随意改变它的属性。当创建“要更改属性”的方法时,需要添加 mutating 关键字,参照:结构 Struct 的 mutating 方法
struct ContentView: View {
var tapCount = 0
var body: some View {
Button("Tap Count: \\(tapCount)") {
tapCount += 1
}
}
}
但是 Swift 不允许改变计算属性,这意味着不能编写 mutating var body: some View 的代码。这似乎陷入了僵局:我们希望能够在程序运行时更改值,但 Swift 不允许这样做,因为视图是结构体。于是 Swift 提供了一个特殊的解决方案,称为属性包装器(即可以在属性之前放置一个特殊属性,从而有效地赋予它超能力)。
@State 解决了结构体的限制:我们无法更改 struct 的属性,因为 struct 是固定的。但 @State 允许 SwiftUI 将该值单独存储在可以修改的地方,而 struct 变成了去引用它。
@state 来标注属性时,SwiftUI 会自动将后面的参数储存在应用程式的某个地方,而后面的代码只是一个指针,指向那个储存的参数。无论在哪里改变储存的参数,都会刷新 body 。这样一来 body 和状态将始终同步,用户界面和状态总是一致。@State 专为存储在一个视图中的简单属性而设计,因此 Apple 建议将 @State 属性标记为 private ,以真正强调它们并非设计为在别处访问。只能从视图本身(或者从被它所调用的函数)內部存取一个状态属性,可以避免视图的用户去存取它。@State private var alertIsVisible: Bool = false
@State private var sliderValue: Double = 80.0
//当将 alertIsVisible 设置为 true 时,会发生什么?那就是改变了应用的状态,所以随后用户界面的更新要保持一致。
//已经用@state标记了的变量,iOS就会自动刷新body 。
//所以你的工作是确保 "body " 考虑到应用程序的状态,并在alertIsVisible为真时,显示一个警报弹出。
<aside>
💡 如果您发现很难记,请尝试:每当在属性包装器中看到“State”时,例如 @State 、 @StateObject 、 @GestureState ,都表示“当前视图拥有此数据”, 它们都应该只和当前视图有关。
</aside>
<aside>
💡 虽然 @State 主要用于值类型,但在 @Observation 宏 机制下也可以将其与引用类型一起使用。这确实意味着每当值更改时,您的视图主体不会被重新调用,但通常没关系。我们实际上把 @State 用作缓存,这样就不会在每次重新创建视图时都一次又一次地重新创建引用对象。这对于不符合 ObservableObject 协议的类来说比较有用。
</aside>
在 SwiftUI 中,每个 @State 状态属性都要有一个初始值,这是因为 @State 属性是用来存储视图自身的状态,必须在初始化时就给它一个合适的初始值。以下是给 @State 状态属性赋【初始值】的常用的方式:
最简单的就是可以在定义@State属性时直接给它一个初始值。
@State private var isPlaying = false
如果视图在代码中没有给 @State 属性赋默认值,可以通过 视图A 实例化视图 B 时补上。
//这种方式每次进入视图B,状态属性初始值都是由视图A传入值决定
//视图A
@State private var value = 99
var body: some View {
DetailView0(number: value)
}
//视图B
struct DetailView0: View {
@State var number: Int
var body: some View {
HStack {
Text("0: \\(number)")
Button("+") { number += 1 }
}
}
}
如果需要对其他视图直接传入的参数,做一些处理加一些逻辑,一般会通过 init 初始化函数进行赋值。
// 写法1:直接写逻辑
struct DetailView1: View {
@State private var number: Int
init(number: Int) {
self.number = number + 1
}
}
// 方式2: 通过 init 初始化方法设置 initialValue
// 在提供的代码中,_name 和_description 使用 State(initialValue:) 来初始化,这是一种设置 @State 属性初始值的方式。
// 状态参数
@State private var name: String
@State private var description: String
// 初始化
init(location: Location, onSave: @escaping (Location) -> Void) {
self.location = location
self.onSave = onSave
_name = State(initialValue: location.name)
_description = State(initialValue: location.description)
}
// 方式3: 通过 init 初始化方法设置 wrappedValue
struct DetailView2: View {
@State private var number: Int
init(number: Int) {
_number = State(wrappedValue: number + 1)
}
var body: some View {
return HStack {
Text("2: \\(number)")
Button("+") { number += 1 }
}
}
}
<aside>
💡 @State 属性包装器用于在视图中管理状态。在给 @State 属性初始化时,需要使用下划线来设置初始值。这是因为 @State 属性是一个属性包装器,它需要通过下划线来访问其包装的值。
</aside>
@State 属性的初始值还可以从环境对象 (@EnvironmentObject) 中获取,和上面一样也是在视图的初始化方法中实现。
由于属性包裹器(如@State)不能用于计算属性,所以会带来一些问题。当我们某个属性是动态变化的(例如获取当前时间作为某控件默认值),但又支持用户使用控件去修改它(通过DatePicker去调整默认值),就不好办了。于是可以:
新增计算属性:去获取现在时间;
新增储存属性:把前面的计算属性赋值给它,并给它赋予 @State 包裹器,让它去做绑定;
// 例如定义了一个计算属性
var defaultWakeTime: Date {
var components = DateComponents()
components.hour = 7
components.minute = 0
return Calendar.current.date(from: components) ?? .now
}
//然后将其赋予某个状态属性,以便后面和UI控件进行绑定
@State private var wakeUp = defaultWakeTime
//这时如果尝试编译该代码,会发现它失败了。原因是我们从一个实例属性内部访问另外一个实例属性,在具体实例化之前,Swift其实不知道这些属性将以什么顺序创建,所以这是无效的。
修复方案很简单:可以将 defaultWakeTime 设为静态变量,这意味着它属于 ContentView 结构本身,而不是该结构的单个实例。这反过来意味着 defaultWakeTime 可以随时读取,因为它不依赖于任何其他属性的存在。于是加上 static 静态关键词
static var defaultWakeTime: Date {
var components = DateComponents()
components.hour = 7
components.minute = 0
return Calendar.current.date(from: components) ?? .now
}
@State private var wakeUp = defaultWakeTime
<aside>
💡 关于 Struct 中静态属性和非静态属性的相互引用,看这里:静态属性/方法 static
【静态属性/方法】不能引用【非静态属性/方法】;而从【非静态代码】访问【静态代码】需要加上类型的名称或者 Self
</aside>
状态属性可以用于 .animation 动画修饰符中,关联后,所有用到该状态值的 UI 属性,都会有动画效果。

Swift 会区分 “仅显示属性的值” 和 “显示属性的值,但同时将任何更改写回该属性” 两种情况,这就是双向绑定。我们用美元 $ 符号标记这些双向绑定,以便它们更明显。这告诉 Swift 它应该读取属性的值,但也应该在发生任何更改时将其写回。
因此,当您在属性名称之前看到美元符号时,请记住它创建了双向绑定:可以读取属性的值,也可以写入该属性的值。
//例子1:
struct ContentView: View {
@State private var name = ""
var body: some View {
Form {
//1. 这里是双向绑定,即可以显示,而对文本字段的任何更改也会更新到 @State 属性中
TextField("Enter your name", text: $name)
//2. 请注意这里不是双向绑定(使用 name 而不是 $name),因为这里只想读取值,而没有要求可以修改值
Text("Your name is \\(name)")
}
}
}
//例子2
.alert(isPresented: $alertIsVisible) { () -> Alert in
var roundValue: Int = Int(self.sliderValue.rounded())
return Alert(title: Text("Hello there!"), message: Text("The slider's value is \\(roundValue)"), dismissButton: .default(Text("Awesome")))
}
Slider(value: self.$sliderValue, in: 1 ... 100)
<aside>
💡 TextField 有一个参数是:text: Binding<String>) ,所以它是要用到绑定参数的。而 text 就没有这个类型的参数。
</aside>
使用常量绑定修复预览:预览
当你使用到双向绑定时,预览模块经常会报错,因为它需要一个确定的值进行预览。这时使用常量绑定很有帮助:虽然硬编码值不会更改,但可以仍像常规绑定一样用,让你的预览代码可以正常工作。
Toggle(isOn: .constant(true)) {
Text("Show advanced options")
}
// 这些常量绑定有各种类型:布尔值、字符串、整数等等,Swift 将确保您为每种视图类型使用正确的类型。
例如以下代码:希望每当文本模糊属性值改变时,打印一条消息。这时会发现:当拖动滑块时,文本模糊量发生了变化,但 print() 语句不会被触发,什么也不会输出。但如果尝试按下按钮,将看到打印一条消息。
@State private var blurAmount = 0.0 {
didSet { print("New value is \\(blurAmount)") }
}
VStack {
Text("Hello").blur(radius: blurAmount)
Slider(value: $blurAmount, in: 0...20)
Button("Random Blur") {
blurAmount = Double.random(in: 0...20)
}
}
属性包装器,其实是将属性包装在另一个结构中。这意味着当使用 @State 来包装字符串时,得到的属性类型是 State<String> 。类似地当使用 @Environment 和其他内容时,最终会得到一个 Environment 类型的结构。之前说过不能修改视图中的属性,因为它们是 structs 。那现在 @State 本身又会生成一个 structs,那怎么样可以修改该结构体呢?
// 使用 Cmd+Shift+O 快速访问(它可以在项目或已导入的框架中查找任何文件或类型),然后输入“State”,选择标记了 SwiftUI 的 State ,可以看到:
@propertyWrapper public struct State<Value> : DynamicProperty {
public var wrappedValue: Value { get nonmutating set }
...
}
<aside> 💡 结论:因此希望观察绑定属性的变化,不应该用观察属性,应该用 onChange 修饰符。参见:监测变化 onChange
</aside>