SwiftUI 学习之propertyWrapper

4,616 阅读2分钟

我正在参加「掘金·启航计划」

属性包装

propertyWrapper(属性包装器)是 Swift 语言特性,它允许我们定义一个自定义类型,该类型包含了 setter 和 getter,我们可以在需要的地方重用它。SwiftUI 中常用的 @State、@Binding等修饰符都是通过属性包装来实现的。

以 @State 为例,看一下其实现

@frozen @propertyWrapper public struct State<Value> : DynamicProperty {

    public init(wrappedValue value: Value)

    public init(initialValue value: Value)

    public var wrappedValue: Value { get nonmutating set }

    public var projectedValue: Binding<Value> { get }
}

init(initialValue:) ,wrappedValue  和 projectedValue  构成了一个 propertyWrapper 最重要的部分。

关于这部分我会在以后的文章中陆续更新。

仅仅是源码还不足以使我们了解和掌握 propertyWrapper 是如何实现属性包装的,下面来组装一个属于自己的“State”。

使用 propertyWrapper 组装自己的“State”

假设我们要给 App 添加额外的日志,每次属性更改时,都将其新值打印到控制台,以此来跟踪操作的有效性,那么以前我们大概率会这么做:

struct Bar {
    private var _x = 0
    var x: Int {
        get { _x }
        set {
            _x = newValue
            print("New value is \(newValue)") 
        }
    }
}

var bar = Bar()
bar.x = 1 // Prints 'New value is 1'

大多数人应该能很快发现这么做存在的问题。是的,这并不是一个通用的方法,实际操作中,既降低开发效率又会让我们对重复代码产生不良情绪。

可以借助泛型来做优化

struct ConsoleLogged<Value> {
    private var value: Value
    
    init(wrappedValue: Value) {
        self.value = wrappedValue
    }
    
    var wrappedValue: Value {
        get { value }
        set { 
            value = newValue
            print("New value is \(newValue)") 
        }
    }
}

现在就可以像下面这样来使用了

struct Bar {
    private var _x = ConsoleLogged<Int>(wrappedValue: 0)
    var x: Int {
        get { _x.wrappedValue }
        set { _x.wrappedValue = newValue }
    }
}
var bar = Bar()
bar.x = 1 // Prints 'New value is 1'

但是这样代码仍不够简洁,这很不 Swift。现在就轮到 @propertyWrapper 这个大杀器出场了。

@propertyWrapper
struct ConsoleLogged<Value> {

/// 这块代码和上面一模一样
    private var value: Value
    init(wrappedValue: Value) {
        self.value = wrappedValue
    }
    
    var wrappedValue: Value {
        get { value }
        set { 
            value = newValue
            print("New value is \(newValue)") 
        }
    }
}

使用时与 @State 一样

struct Bar {
    @ConsoleLogged var x = 0
}
var bar = Bar()
bar.x = 1 // Prints 'New value is 1'

@ConsoleLogged 实际上就是被包装了一个语法糖,底层将 x 属性包装到了一个 ConsoleLogged<Int> 中,并保留了使用者对其进行操作的可能性,可参考上面泛型优化后的使用代码。

关于 ConsoleLogged 的初始化方法,当 wrappedValue 出现在 init 方法的第一个参数位置时,编译器允许我们在声明的时候直接为 @ConsoleLogged var x 进行赋值,在 State 的定义中源码注释说明了不要直接调用初始化方法

Don't call this initializer directly. Instead, declare a property with the State attribute, and provide an initial value:

@State private var isPlaying: Bool = false

参考:

1.The Complete Guide to Property Wrappers in Swift 5

2.《SwiftUI 和 Combine 编程 第二版》王巍