我正在参加「掘金·启航计划」
属性包装
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 编程 第二版》王巍