2

Swift 中的 propertyWrapper

 1 year ago
source link: https://kingcos.me/posts/2022/property_wrapper/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Release Notes ↕

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 中均已经找不到该协议的内容。

由上我们可以得出以下结论:

  1. 使用赋值 @State 变量的方式是通过 init(wrappedValue:) 初始化的;
  2. 访问或更新 @State 变量值的本质是通过对 wrappedValue 访问或更新的;
  3. 通过 $ 访问 @State 变量的本质是 projectedValue,投射值使得原本的值类型变量具有了引用语义;
  4. @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 的代码更加优雅。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK