@propertyWrapper(属性包装器)
@propertyWrapper(属性包装器)
在swiftUI中大量使用了属性包装器,用来监控数据变化,从而更新UI的@State包装器等等。
通过@propertyWrapper注解,我们也可以实现自定义的属性包装,它可以应用在class、struct、enum类型上,下面我们通过struct来实现一个自定义的属性包装器。
属性包装器的类,必须要实现一个wrappedValue名字的计算型属性,用来获取被包装属性的值,以及需要一个带有参数标签名为wrappedValue的初始化方法,如下:
@propertyWrapper struct DrPropertyWrapper<T>{ private var wapper: DrValueWrapper<T> // 这里是被包装的属性值的包装类 var wrappedValue: T { get{ wapper.value } nonmutating set{ // nonmutating标记,告诉编译器,该方法不会导致struct值发生变化,否则编译无法通过 wapper.value = newValue } } init(wrappedValue: T){ // 这里的参数标签名必须为:wrappedValue wapper = DrValueWrapper(wrappedValue) } }
这样就完成了一个自定义的属性包装器,使用如下:
struct AppView{ @DrPropertyWrapper var name: String // 使用属性包装器 }
实际上编译器自动为我们实现了如下操作:
1、实例化一个属性包装器对象:DrPropertyWrapper(wrappedValue: name)
2、将此属性包装器实例命名为_name的私有属性,命名规则就是在被包装的属性名前,添加下划线前缀
func runTest() -> Void{ let app = AppView(name: "MainView") app.name = "SecondView"; // 实际上是调用了属性包装器对象的wrappedValue计算型属性的set方法 print("name: \(app.name)") // 实际上是调用了属性包装器对象的wrappedValue计算型属性的get方法 }
因为自动生成的属性_name是私有的,所以这里app._name是找不到的,必须在AppView内部调用才可以。
回到属性包装器的定义部分,我们知道struct是一个值类型,所以,其内部的值一旦发生改变,其类型值也将发生改变,为了让struct的属性值改变,但不会生成一个新的struct值类型,就需要将这个属性类型定义为指针类型,即class,并且在修改struct属性值的方法前,需要通过nonmutating来告诉编译器,这个属性的赋值,不会导致struct的改变。
@propertyWrapper struct DrPropertyWrapper<T>{ private let wapper: DrValueWrapper<T> // 这里是被包装的属性值的包装类 } // 这里是一个class类型的值包装类 final class DrValueWrapper<T>{ var value: T init(_ val: T) { value = val } }
我们可以为属性包装器类通过扩展,添加自定义方法,如下:当被包装的属性类型为String时,我们的属性包装器增加如下方法:
extension DrPropertyWrapper where T: StringProtocol { // 这里我们采用了callAsFunction提供了一种对象形式调用方法 func callAsFunction(append str: String) -> String{ return wapper.value.appending(str); } }
关于callAsFunction可以参考:《@dynamicCallable与callAsFunction的区别》
然后,我们就可以调用属性包装器对象的方法了
struct AppView{ @DrPropertyWrapper var name: String func printName() { print(name) // 属性包装器的值 print(_name.wrappedValue) // _name:属性包装器对象,即:DrPropertyWrapper<String> } func callAsFunction(appendForName str: String) -> String { let _str = _name(append: str) // 调用属性包装器对象的方法 name = _str return _str } }
最后我们可以为属性包装器增加一个投影值,该值为任意类型,可认为是对属性包装器的一种扩展,要获取扩展的投影值,我们只需要在用属性包装器修饰的变量名前使用$符号,即可取出该包装器的投影值。举例如下:
有时我们想获取属性包装器对象本身,那么我们就可以为这个属性包装器增加一个投影值,如下:
@propertyWrapper struct DrPropertyWrapper<T>{ private let wapper: DrValueWrapper<T> // 这里是被包装的属性值的包装类 // 新增包装器的投影值,这里返回包装器自身 var projectedValue: DrPropertyWrapper<T> { self } }
然后获取投影值就像下面这样:
struct AppView{ @DrPropertyWrapper var name: String func printProjectedValue() { print($name) // 属性包装器的投影值,这里是包装器自身DrPropertyWrapper } }
至于官方提供的@State属性包装器注解,是用来提供SwiftUI当属性值改变时,自动刷新UI的,实现原理后面我们会再次分析。