在 Swift 中,这一特性的正式名称是属性包装 (Property Wrapper)。不论是 @State,@Binding,或者是我们在下一节中将要看到的 @ObjectBinding 和 @EnvironmentObject,它们都是被 @propertyWrapper 修饰的 struct 类型。以 State 为例,在 SwiftUI 中 State 定义的关键部分如下:
@propertyWrapper
public struct State<Value> :
DynamicViewProperty, BindingConvertible
{
public init(initialValue value: Value)
public var value: Value { get nonmutating set }
public var wrappedValue: Value { get nonmutating set }
public var projectedValue: Binding<Value> { get }
}
//init(initialValue:),wrappedValue 和 projectedValue 构成了一个 propertyWrapper 最重要的部分。在 @State 的实际使用里:
struct ContentView : View {
// 1
@State
private var brain: CalculatorBrain = .left("0")
var body: some View {
VStack(spacing: 12) {
Spacer()
Text(brain.output) // 2
//...
CalculatorButtonPad(brain: $brain) // 3
//...
}
}
}
1.由于 init(initialValue:) 的存在,我们可以使用直接给 brain 赋值的写法,将一个 CalculatorBrain 传递给 brain。我们可以为属性包装中定义的 init 方法添加更多的参数,我们会在接下来看到一个这样的例子。不过 initialValue 这个参数名相对特殊:当它出现在 init 方法的第一个参数位置时,编译器将允许我们在声明的时候直接为 @State var brain 进行赋值。
2.在访问 brain 时,这个变量暴露出来的就是 CalculatorBrain 的行为和属性。对 brain 进行赋值,看起来也就和普通的变量赋值没有区别。但是,实际上这些调用都触发的是属性包装中的 wrappedValue。@State 的声明,在底层将 brain 属性“包装”到了一个 State 中,并保留外界使用者通过 CalculatorBrain 接口对它进行操作的可能性。
3.使用美元符号前缀 ($) 访问 brain,其实访问的是 projectedValue 属性。在 State 中,这个属性返回一个 Binding 类型的值,通过遵守 BindingConvertible,State 暴露了修改其内部存储的方法,这也就是为什么传递 Binding 可以让 brain 具有引用语义的原因。
ObservableObject 和 @ObjectBinding Link to heading
ObservableObject 协议要求实现类型是 class,它只有一个需要实现的属性:objectWillChange。在数据将要发生改变时,这个属性用来向外进行“广播”,它的订阅者 (一般是 View 相关的逻辑) 在收到通知后,对 View 进行刷新。
创建 ObservableObject 后,实际在 View 里使用时,我们需要将它声明为 @ObservedObject。这也是一个属性包装,它负责通过订阅 objectWillChange 这个“广播”,将具体管理数据的 ObservableObject 和当前的 View 关联起来。