@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的,实现原理后面我们会再次分析。

 

posted @ 2021-06-22 20:20  zbblogs  阅读(817)  评论(0编辑  收藏  举报