什么是@propertyWrapper(属性包装)?

在 Swift 中,这一特性的正式名称是属性包装 (Property Wrapper)。不论是 @State, @Binding,或者是我们在下一节中将要看到的 @ObjectBinding 和 @EnvironmentObject,它们都是被 @propertyWrapper 修饰的 struct 类型。

以 State 为例,在 SwiftUI 中 State 定义的关键部分如下:

1.State定义

@main
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen @propertyWrapper public struct State<Value> : DynamicProperty {
    public init(initialValue value: Value)
    public init(wrappedValue value: Value)
    public var wrappedValue: Value { get nonmutating set }
    public var projectedValue: Binding<Value> { get }
}

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

示例:

struct ContentView : View {

@State //1
private var brain: CalculatorBrain = .left("0")
var body: some View { VStack(spacing: 12) {
      Spacer()
      Text(brain.output) // 2
        //...
      CalculatorButtonPad(brain: $brain) // 3
        //...
} }
}

应用示例:

实际开发中,我们的默一个属性要求必须为两头去除空格的状态. 这时我们有两种方式实现

1, 重写get方法,每次get时都去除两头空格

2, 重写set方法,每次set时都去除两头空格

class Test {
    var whiteStr {
        set {
            // 去除空格操作
        }
        // 或者
        get {
            // 去除空格操作
        }
    }
}

这时我们会面临一个问题,如果有很多个属性都有这个要求,那么我们的代码就会变成

class Test {
    var whiteStr1 {
        set {
            // 去除空格操作
        }
        // 或者
        get {
            // 去除空格操作
        }
    }
    
    var whiteStr2 {
        set {
            // 去除空格操作
        }
        // 或者
        get {
            // 去除空格操作
        }
    }
    ...
}

充其量我们把“去除空格操作”抽象提取出来,但也并不能降低多少代码重复率,还是在不停的写set get

这时@propertyWrapper的作用就提现出来了

class Test {
    @NoWhite() //“NoWhite”这个名字是我们自己定义的
    var whiteStr1 
    
    @NoWhite()
    var whiteStr2
    
    ...
}

@propertyWrapper怎么实现及实践

@propertyWrapper struct NoWhite {
    // 用来记录被包装属性的值
    // 可以是别的名字,比如model item等,value只是在此demo中的名字
    var value: String
    
    // 包装值,可以把它看成value的计算属性
    // 'wrappedValue' 这个名称不能更改,必须实现
    // 以上一段中的`whiteStr`为例
    // 被NoWhite修饰后, 操作`whiteStr`对应的get set 方法,实际是在操作wrappedValue的
    // get set 方法
    // 这里我们选用在set中处理两头空格的方式
    var wrappedValue: String {
        get { value }
        set {
            let whitespace = NSCharacterSet.whitespacesAndNewlines
            value = newValue.trimmingCharacters(in: whitespace)
        }
    }
    
    //  映射值 (只写get方法)
    //  被修饰的属性在属性名前加`$`符,调用的就是的projectedValue的get方法
    //  比如, self.$whiteStr
    //  当使用self.$whiteStr作为参数传递时,是引用传递. 
    //  使用self.whiteStr作为参数传递时,是值传递
     var projectedValue: String {
        let whitespace = NSCharacterSet.whitespacesAndNewlines
        return value.trimmingCharacters(in: whitespace)
    }
    
    // 初始化方法
    init(wrappedValue initialValue: String = "") {
        value = initialValue
    }

}

示例:

class Test {
    @NoWhite()
    var whiteStr
}

te.whiteStr = " 123 " // 首位有空格

print(te.whiteStr) // "123"

print(te.whiteStr.count) // "3"

总结

1, @propertyWrapper的本质就是包装修饰属性的set get方法

2, @propertyWrapper的作用就是降低代码重复率

3, @propertyWrapper的应用中的projectedValue在SwiftUI的 @State中, 作为 @Binding 的修饰对象的引用传递来解决多层嵌套UI的状态同步问题.

可参考: 喵神的美元人民币转换