代码示例
首先分享一段代码示例,下面的代码内容为通过Slider滑动或者点击Button按钮,改变Text文本内容的模糊程度。
struct ContentView: View {
@State private var blurAmount = 0.0
var body: some View {
VStack {
Text("Hello, World!")
.blur(radius: blurAmount)
Slider(value: $blurAmount, in: 0...20)
.onChange(of: blurAmount) { newValue in
print("New value is \(newValue)")
}
Button("Random Blur") {
blurAmount = Double.random(in: 0...20)
}
}
}
}
在blurAmount中添加didSet重新观察输出内容:
@State private var blurAmount = 0.0 {
didSet {
print("New value is \(blurAmount)")
}
}
会发现只有在视图点击Button的时候,才会调用didSet闭包。拖动Slider并不会调用didSet。
Slider没有触发didSet的原因
在 SwiftUI 中,使用 @State 声明的属性是专门为视图的本地状态设计的。这些属性是由 SwiftUI 管理的,并且它们的值更改不会触发 didSet 属性观察器。原因是 @State 的实际存储由 SwiftUI 框架控制,而不是自己声明的属性存储。
具体来说,didSet 不会被调用的原因是 SwiftUI 对 @State 的更改进行了特殊的处理,它直接将新的值注入到 State 的存储中,而不是通过传统的 Swift 属性赋值逻辑,这绕过了 didSet。
什么是@State
@State 是 SwiftUI 提供的属性包装器,用于管理视图的本地状态。其作用是:
存储视图状态值:状态值被存储在 SwiftUI 管理的内存中,而不是对象的普通属性。
驱动视图重绘:当 @State 的值发生更改时,SwiftUI 会自动重新计算依赖该值的视图,并更新 UI。
例如:
@State private var count: Int = 0
在编译后,@State 的行为类似于一个包装器,它在视图结构中维护状态值。这种特殊机制让 @State 的存储位置和生命周期不同于普通的类或结构体属性。
@State 如何管理状态?
@State 的实际值存储在 SwiftUI 的内部环境中,而不是视图结构本身。这种存储方式有以下特点:
SwiftUI 会为每个 @State 创建一个专用的存储容器。
当视图的 body 被重新计算时,@State 的值会自动绑定到正确的存储容器中。
通过 $ 获取 Binding,让 UI 控件能够直接读写状态值。
因此,当 @State 的值改变时,SwiftUI 是通过底层容器更新值,而不是直接修改视图的属性。
为什么 didSet 不会触发?
didSet 是 Swift 属性的一部分,用于监听普通属性的值变化。它的触发条件是通过属性赋值语法 (self.property = newValue) 修改值。然而:
@State 的值并不是直接存储在视图的属性中,而是存储在 SwiftUI 的专用容器中。
当 @State 的值更新时,SwiftUI 会直接操作容器内部的值,而不是通过属性赋值。这种操作绕过了 Swift 属性观察器的调用。
换句话说,@State 的更改是 由 SwiftUI 管理的内部机制触发,而不是通过标准的 Swift 属性赋值流程。
didSet触发条件
属性观察器(didSet 和 willSet)是在属性的值被修改时触发的。这种修改必须通过显式的赋值语法完成,例如:
self.property = newValue
对属性进行赋值操作时,Swift 会自动调用 didSet。这是因为属性赋值是 Swift 编译器内置的一个明确的触发点。
因为:
1、Swift 属性观察器是与属性赋值逻辑绑定的。
2、只有赋值操作才能触发属性存储的变更逻辑,继而调用 didSet。
3、其他非赋值操作绕过了编译器对属性存储的管理,因此不会触发 didSet。
SwiftUI容器
@State 的存储容器是 SwiftUI 内部管理的一个机制,旨在为声明式 UI 编程模型提供状态管理支持。
@State 容器存在在哪里?
@State 的容器并不直接存在于视图的结构中,而是由 SwiftUI 的运行时环境动态管理。它的存储位置和作用范围主要包括以下几个方面:
1、绑定到视图的生命周期:
当一个 SwiftUI 视图被初始化时,SwiftUI 会为其 @State 属性创建一个专用的存储容器。
这个存储容器与视图的生命周期绑定,但独立于视图的具体实现。
2、在 SwiftUI 内部的状态缓存中:
SwiftUI 维护了一个内部状态缓存系统,用于存储和管理 @State 值。
这些值与视图树关联,并通过键值或其他标识符在视图树中查找和更新。
3、独立于视图实例的重建:
当 SwiftUI 重新计算视图的 body(例如,视图被重新渲染时),原来的 @State 容器依然保留,确保状态在视图重建过程中不会丢失。
容器中存储的内容是什么?
@State 容器的核心是一个简单的值存储。具体来说:
1、实际存储的状态值:
容器存储的是 @State 属性所管理的值,例如 Int、String、Double 或自定义类型。
值是直接存储的,SwiftUI 在需要时访问和更新这些值。
2、绑定机制:
容器还存储了与绑定 (Binding) 相关的信息。通过 $ 访问的绑定实际上是对容器的引用,允许 UI 控件双向绑定到状态值。
3、变更通知机制:
容器还会跟踪值的变更。当值发生变化时,SwiftUI 会标记相关视图为“需要更新”,从而触发 UI 重绘。
容器的作用是什么?
@State 容器的作用可以总结为以下几点:
1、状态管理:
容器确保视图的状态值在视图生命周期内是持久的,即使视图本身被重新计算。
2、UI 重绘驱动:
当 @State 的值发生变化时,SwiftUI 会监听到这个变化,并根据声明式 UI 的依赖关系自动触发视图的更新。
3、绑定支持:
容器通过 Binding 提供了双向数据流支持,使得 UI 控件和状态值之间可以进行动态交互。
4、简化开发者的工作:
开发者不需要手动管理状态值的存储或更新,也不需要触发视图的重绘。@State 的容器自动完成这些工作。
容器的内部实现(推测)
SwiftUI 是一个封闭的框架,其内部实现未公开,但从其行为和设计可以推测:
1、哈希或标识符管理:
每个 @State 属性通过某种哈希值或标识符与视图树关联。这确保即使视图被重建,状态也能被正确恢复。
2、值存储和观察:
SwiftUI 使用值类型(如 Double、Int)进行轻量存储。
值变化会触发 SwiftUI 内部的观察机制。
3、增量更新:
SwiftUI 会根据状态的变化确定哪些视图需要重新计算,从而优化性能。
解决方案
为了解决前面的代码示例,可以考虑以下两种方式:
使用 onChange 修饰符
SwiftUI 提供了一个 onChange(of:perform:) 修饰符,可以用于监听状态值的更改:
struct ContentView: View {
@State private var blurAmount = 0.0
var body: some View {
VStack {
Text("Hello, World!")
.blur(radius: blurAmount)
Slider(value: $blurAmount, in: 0...20)
.onChange(of: blurAmount) { _, newValue in
print("New value is \(newValue)")
}
Button("Random Blur") {
blurAmount = Double.random(in: 0...20)
}
}
}
}
需要注意的是onChange在iOS14后,推荐使用接受旧值和新值的完整形式:
.onChange(of: someValue) { oldValue, newValue in
// 响应变化,并可使用旧值和新值
}
之前的只接受新值的简化形式被标记为废弃,从iOS17开始不建议使用。
使用 Binding 自定义行为
通过包装一个 Binding,可以在更改值时插入自定义逻辑:
struct ContentView: View {
@State private var blurAmount = 0.0
var body: some View {
VStack {
Text("Hello, World!")
.blur(radius: blurAmount)
Slider(value: Binding(
get: { blurAmount },
set: { newValue in
blurAmount = newValue
print("New value is \(newValue)")
}
), in: 0...20)
Button("Random Blur") {
blurAmount = Double.random(in: 0...20)
}
}
}
}
由于 @State 的特殊实现,didSet 不会在其值变化时触发。如果需要响应状态值变化,可以使用 onChange 修饰符或者自定义 Binding,这两种方式都适合 SwiftUI 的声明式编程模式。
解决方案背后的机制
如果想在 @State 值更改时执行逻辑操作,SwiftUI 提供了更符合声明式编程模型的方式,比如 onChange 和自定义 Binding。它们通过在值更新时显式插入逻辑来替代传统的 didSet。
onChange 的实现机制
onChange 是一个 SwiftUI 修饰符,它监听指定值的变化,并在值更新后触发回调:
.onChange(of: value) { newValue in
// 执行逻辑
}
内部原理是 SwiftUI 监视 value 的变化并触发回调。这是一种响应式的、与 @State 机制契合的设计。
自定义 Binding 的实现机制
通过 Binding 包装,可以将值的读取和写入操作显式控制:
Binding(
get: { self.value },
set: { newValue in
self.value = newValue
// 插入自定义逻辑
}
)
在这里,get 和 set 是明确的闭包,它们允许拦截值的变化,而不是依赖 didSet。
总结
@State 的值变化不会触发 didSet 的核心原因在于:
1、@State 的值存储在 SwiftUI 的专用容器中,而不是视图的属性中。
2、SwiftUI 更改 @State 的值时,绕过了普通的 Swift 属性赋值逻辑,因此不会触发 didSet。
正确响应 @State 的变化需要使用 SwiftUI 的响应式工具(如 onChange 和 Binding),这符合 SwiftUI 的声明式编程范式。
相关文章
How property wrappers become structs:https://www.hackingwithswift.com/books/ios-swiftui/how-property-wrappers-become-structs