Preface
SwiftUI 中,我们经常可以见到 @State
、@Binding
等类似 @
开头的代码,其官方名称为 Property Wrapper —— 属性包装器。
@State
在 Xcode 中,我们可以进入到 @State
的定义中,即:
/// A property wrapper type that can read and write a value managed by SwiftUI.
/// 属性包装类型指可以读取和写入由 SwiftUI 管理的一个值。
///
/// SwiftUI manages the storage of a property that you declare as state. When
/// the value changes, SwiftUI updates the parts of the view hierarchy that
/// depend on the value. Use state as the single source of truth for a given
/// value stored in a view hierarchy.
/// SwiftUI 管理声明为状态的属性存储。当值更改时,SwiftUI 会更新视图层次结构中依赖于该值的部分。使用状态作为存储在视图层次结构中的给定值的单一可信源(single source of truth)。
///
/// A `State` instance isn't the value itself; it's a means of reading and
/// writing the value. To access a state's underlying value, refer to it by
/// its property name, which returns the ``State/wrappedValue`` property value.
/// `State` 实例不是值本身;而是一种读写值的方法。要访问一个状态的底层值,可以通过其属性名引用,该属性名返回 ``State/wrappedValue`` 属性值。
/// For example, you can read and update the `isPlaying` state property in a
/// `PlayButton` view by referring to the property directly:
/// 例如,可以通过直接引用 `PlayButton` 视图中的 `isPlaying` 状态属性来读取和更新该属性:
///
/// struct PlayButton: View {
/// @State private var isPlaying: Bool = false
///
/// var body: some View {
/// Button(isPlaying ? "Pause" : "Play") {
/// isPlaying.toggle()
/// }
/// }
/// }
///
/// If you pass a state property to a child view, SwiftUI updates the child
/// any time the value changes in the parent, but the child can't modify the
/// value. To enable the child view to modify the stored value, pass a
/// ``Binding`` instead. You can get a binding to a state value by accessing
/// the state's ``State/projectedValue``, which you get by prefixing the
/// property name with a dollar sign (`$`).
/// 如果将状态属性传递给子视图,则当父视图中的值更改时,SwiftUI 会随时更新子视图,但子视图不能修改值。启用子视图修改存储的值,改为传递一个 ``Binding``。可以通过访问状态的 `state/projectedValue`` 来获得与状态值的绑定,该值是通过在属性名称前面加上美元符号(`$`)获得的。
///
/// For example, you can remove the `isPlaying` state from the play button in
/// the example above, and instead make the button take a binding to the state:
/// 例如,可以从上述示例中的播放按钮中删除 `isPlaying` 状态,并使按钮绑定到该状态:
///
/// struct PlayButton: View {
/// @Binding var isPlaying: Bool
///
/// var body: some View {
/// Button(isPlaying ? "Pause" : "Play") {
/// isPlaying.toggle()
/// }
/// }
/// }
///
/// Then you can define a player view that declares the state and creates a
/// binding to the state using the dollar sign prefix:
/// 然后,可以定义一个播放器视图,该视图声明状态并使用美元符号前缀创建与状态的绑定:
///
/// struct PlayerView: View {
/// var episode: Episode
/// @State private var isPlaying: Bool = false
///
/// var body: some View {
/// VStack {
/// Text(episode.title)
/// .foregroundStyle(isPlaying ? .primary : .secondary)
/// PlayButton(isPlaying: $isPlaying) // Pass a binding. 传递绑定
/// }
/// }
/// }
///
/// Don't initialize a state property of a view at the point in the view
/// hierarchy where you instantiate the view, because this can conflict with
/// the storage management that SwiftUI provides. To avoid this, always
/// declare state as private, and place it in the highest view in the view
/// hierarchy that needs access to the value. Then share the state with any
/// child views that also need access, either directly for read-only access,
/// or as a binding for read-write access.
/// 不要在视图层次结构中实例化视图时,初始化视图的状态属性,因为这可能与 SwiftUI 提供的存储管理冲突。为避免这种情况,需要始终将状态声明为 private,并将其放置在需要访问该值的视图层次结构中的最顶层视图中。然后,将状态与任何也需要访问的子视图共享,可以直接用于只读访问,也可以作为读写访问的绑定。
///
/// You can safely mutate state properties from any thread.
/// @State 是线程安全的。
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen @propertyWrapper public struct State<Value> : DynamicProperty {
/// Creates the state with an initial wrapped value.
/// 使用初始包装值创建状态。
///
/// Don't call this initializer directly. Instead, declare a property
/// with the ``State`` attribute, and provide an initial value:
/// 不要直接调用该构造方法。取而代之的是使用 ``State`` 声明属性,并提供初始值:
///
/// @State private var isPlaying: Bool = false
///
/// - Parameter wrappedValue: An initial wrappedValue for a state.
public init(wrappedValue value: Value)
/// Creates the state with an initial value.
///
/// - Parameter value: An initial value of the state.
public init(initialValue value: Value)
/// The underlying value referenced by the state variable.
/// 由状态变量引用的底层值。
///
/// This property provides primary access to the value's data. However, you
/// don't access `wrappedValue` directly. Instead, you refer to the property
/// variable created with the ``State`` attribute. In the following example,
/// the button's label depends on the value of `isPlaying` and its action
/// toggles the value of `isPlaying`. Both of these accesses implicitly
/// rely on the state property's wrapped value.
/// 该属性提供对值数据的主要访问。但是不能直接访问 `wrappedValue`。取而代之,引用了使用 `` State`` 属性创建的属性变量。在以下示例中,按钮的标签取决于 `isPlaying` 的值,以及点击操作将切换 `isPlaying` 的值。这两种访问都隐式地依赖于状态属性的包装值。
///
/// struct PlayButton: View {
/// @State private var isPlaying: Bool = false
///
/// var body: some View {
/// Button(isPlaying ? "Pause" : "Play") {
/// isPlaying.toggle()
/// }
/// }
/// }
///
public var wrappedValue: Value { get nonmutating set }
/// A binding to the state value.
/// 状态值的绑定。
///
/// Use the projected value to pass a binding value down a view hierarchy.
/// To get the `projectedValue`, prefix the property variable with a dollar
/// sign (`$`). In the following example, `PlayerView` projects a binding
/// of the state property `isPlaying` to the `PlayButton` view using
/// `$isPlaying`:
/// 使用投射值向下传递视图层次结构的绑定值。要获取 `projectedValue`,可在属性变量前加美元符号(`$`)。在下面的示例中,`PlayerView` 使用 `$isPlaying` 将状态属性 `isPlaying` 的绑定投影到 `PlayButton` 视图:
///
/// struct PlayerView: View {
/// var episode: Episode
/// @State private var isPlaying: Bool = false
///
/// var body: some View {
/// VStack {
/// Text(episode.title)
/// .foregroundStyle(isPlaying ? .primary : .secondary)
/// PlayButton(isPlaying: $isPlaying)
/// }
/// }
/// }
///
public var projectedValue: Binding<Value> { get }
}
注意
在许多之前的书籍或内容中,
State
结构体还遵守了BindingConvertible
协议,但从 Xcode 13 以及 Xcode 14-beta 中均已经找不到该协议的内容。
由上我们可以得出以下结论:
- 使用赋值
@State
变量的方式是通过init(wrappedValue:)
初始化的; - 访问或更新
@State
变量值的本质是通过对wrappedValue
访问或更新的; - 通过
$
访问@State
变量的本质是projectedValue
,投射值使得原本的值类型变量具有了引用语义; @State
变量要使用private
修饰,当需要子组件更新值时,需要声明为@Binidng
并使用$
按引用传值。
另外,有人可能和我一样有疑问,为什么 @State
提供了两个类似的初始化方法?答案是最初的提议是通过 init(initialValue:)
实现的,而后改为了 init(wrappedValue:)
。但由于向后兼容,前者也得以保留。具体也可见 Swift 官方论坛的讨论:SwiftUI.State: .init(wrappedValue:) vs. .init(initialValue:): what’s the difference?。
自定义 propertyWrapper
属性包装器的本质类似于自定义 getter 和 setter。
@propertyWrapper struct WrapperDemo {
var value: Int = 0
var wrappedValue: Int {
get {
value
}
set {
value = newValue
}
}
init(wrappedValue: Int) {
print(#function, wrappedValue)
self.wrappedValue = wrappedValue
}
init(initialValue: Int) {
print(#function, initialValue)
self.wrappedValue = initialValue
}
}
struct ModelDemo {
// init(initialValue: Int)
@WrapperDemo(initialValue: 10) var d1
// public init(wrappedValue: Int)
@WrapperDemo var d2 = 20
func test() {
print(d1) // #1 0x0000000100006040 in ModelDemo.d1.getter ()
print(d2)
}
}
let d = ModelDemo()
d.test()
// init(initialValue:) 10
// init(wrappedValue:) 20
// 10
// 20
如上,我们也可以封装属性包装器,使得依赖 getter 和 setter 的代码更加优雅。