(三) SwiftUI - property wrapper

1,279 阅读5分钟

SwiftUI - property wrapper

设计动机

有很多的属性实现模式都是重复的,所以我们需要一套属性机制能够定义这些重复的模式,并且使用体验能和 Swift 提供的属性模式类似,例如 Swift 提供的 lazy@NSCopying。另外,lazy@NSCopying 的使用范围有限、很多情况下不实用。

lazy 是 Swift 的一个重要特性,如果我们希望实现不可变的懒加载属性,lazy是不能实现的。 @NSCopying 和 OC 里面的 copy 关键字功能一样,给属性赋值会调用 NSCopying.copy() 方法。

/// Swift
@NSCopying var employee: Person
// Equal to OC
@property (copy, nonatomic) Person *employee;

@NSCopying 修饰属性,会将代码转为下面这样:

// @NSCopying var employee: Person
  var _employee: Person
  var employee: Person {
    get { return _employee }
    set { _employee = newValue.copy() as! Person }
  }

其实是在 set 方法中进行 copy 操作,这会导致一个问题,在初始化方法中无法实现 copy 功能

init( employee candidate: Person ) {
   /// ...
    self.employee = candidate // 浅拷贝
 /// ...
}

在 OC 里可以通过 _propertyself.property 控制是直接访问成员变量还是访问 setter 方法,但 Swift 里,在初始化方法中访问属性时总是直接访问成员变量,不能访问 setter 方法,即时用 self. 语法也没用。不能调用 setter 方法,这就直接导致不能调用 @NSCopyingcopy 方法实现深拷贝。通常的做法是在初始化方法里面手动调用 copy 方法,但这很容易出错。

init( employee candidate: Person ) {
   // ...
   self.employee = candidate.copy() as! Person
   // ...
}

什么是 Property Wrapper ?

就是将属性用 wrapper 类型包裹一层。可以将属性定义、管理属性存储的代码分开,管理的代码只要写一次,就可以用在多个属性上,让属性自己决定使用哪个 wrapper。

自定义 wrapper 只需在自定义类型前添加 @propertyWrapper 标记:

@propertyWrapper
struct Lazy<T> {
    var wrappedValue: T
}

上面的 Lazy 就可以对其他属性进行标记:

struct UseLazy {
    @Lazy var foo: Int = 1738
}

上面的代码会转换为:

struct UseLazy {
    private var _foo: Lazy<Int> = Lazy<Int>(wrappedValue: 1738)
    var foo: Int {
      get { return _foo.wrappedValue }
      set { _foo.wrappedValue = newValue }
    }
}

foo 实际上会变成 _foo: Lazy 类型的变量,并且会生成 foo 的 get、set 方法,在get、set 方法中访问 _foo.wrappedValue,所以自定义 wrapper 的关键就是在 wrappedValue 的 get、set 方法中实现需要的逻辑。

另外,我们可以为自定义的 wrapper 提供更多 API:

@propertyWrapper
struct Lazy<T> {
    var wrappedValue: T
    func reset() -> Void { ... } // 添加一个方法
}

由于 Wrapper 生成的 private var _foo: Lazy<Int> = Lazy<Int>(wrappedValue: 1738) 成员变量是私有的,所以只能在 UseLazy 结构体内部调用 func reset() -> Void 方法:

struct UseLazy {
    func useReset() {
        _foo.reset()
    }
}

如果想在外界这样调用则是不可以的:

func myfunction() {
    let u = UseLazy()
    // 不能直接访问 private var _foo:
    u._foo.reset()
}

如果想在外界调用 wrapper 的API,需要借助 projectedValue,需要在自定义的 wrapper 类型中实现 projectedValue 属性:

@propertyWrapper
struct Lazy<T> {
    var wrappedValue: T
    public var projectedValue: Self {
        get { self }
        set { self = newValue }
    }
    func reset() { ... }
}

声明 projectedValue 后,@Lazy var foo: Int = 1738 会转换成下面的代码,会多生成 $foo 的 get、set 方法

private var _foo: Lazy<Int> = Lazy<Int>(wrappedValue: 1738)
var foo: Int {
   get { return _foo.wrappedValue }
   set { _foo.wrappedValue = newValue }
}
public var $foo: Lazy<Int> {
   get { _foo.projectedValue }
   set { _foo.projectedValue = newValue }
}

由于 public var $foo: Lazy<Int> 是 public 的,在外界可以通过 $foo 拿到 _foo.projectedValueprojectedValue 的 get 返回 self,所以调用 u.$foo 实际返回的就是 _foo: Lazy。这样在外界就能调用 reset() 方法了。

func myfunction() {
    let u = UseLazy()
    // u.$foo -> _foo.projectedValue -> _foo
    u.$foo.reset()
}

使用场景

  • UserDefault
  • @NSCopying 问题
  • Property Wrapper 限制数据范围
  • 记录数据的变化(projectedValue)

UserDefault

如果想在 UserDefault 中存储一些值,例如存储是否为第一次启动、和字体信息,之前我们可能会这样做:

struct GlobalSetting {
    static var isFirstLanch: Bool {
        get {
            return UserDefaults.standard.object(forKey: "isFirstLanch") as? Bool ?? false
        } set {
            UserDefaults.standard.set(newValue, forKey: "isFirstBoot")
        }
    }
    static var uiFontValue: Float {
        get {
            return UserDefaults.standard.object(forKey: "uiFontValue") as? Float ?? 14
        } set {
            UserDefaults.standard.set(newValue, forKey: "uiFontValue")
        }
    }
}

可以看到上面的代码是重复的,如果要存储更多的信息,会导致更多重复的代码。 有了 property wrapper 之后,上面的问题都可以很容易的解决。

@propertyWrapper
struct UserDefault<T> {
    let key: String
    let defaultValue: T
    
    var wrappedValue: T {
        get {
            UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
        }
        set {
            UserDefaults.standard.set(newValue, forKey: key)
        }
    }
}

struct GlobalSetting {
    @UserDefault(key: "isFirstLaunch", defaultValue: true)
    static var isFirstLaunch: Bool
    
    @UserDefault(key: "uiFontValue", defaultValue: 12.0)
    static var uiFontValue: Float
}

使用 @propertyWrapper 修饰 UserDefault 结构体,UserDefault 就可以修饰其他属性了,isFirstLaunch、uiFontValue 用 @UserDefault 后,其属性的存储就由 UserDefault 来实现。将 isFirstLaunch 展开就是相当于:

struct GlobalSettings {
    static var $isFirstLanch = UserDefault<Bool>(key: "isFirstLanch", defaultValue: false)
    static var isFirstLanch: Bool {
        get {
            return $isFirstLanch.value
        }
        set {
            $isFirstLanch.value = newValue
        }
    }
}

@NSCopying 问题

Person 类型的定义

class Person: NSObject, NSCopying {
    var firstName: String
    var lastName: String
    var job: String?
    
    init( firstName: String, lastName: String, job: String? = nil ) {
        self.firstName = firstName
        self.lastName = lastName
        self.job = job
        
        super.init()
    }
    
    /// Conformance to <NSCopying> protocol
    func copy( with zone: NSZone? = nil ) -> Any {
        let theCopy = Person.init( firstName: firstName, lastName: lastName )
        theCopy.job = job
        
        return theCopy
    }
    
    /// For convenience of debugging
    override var description: String {
        return "\(firstName) \(lastName)" + ( job != nil ? ", \(job!)" : "" )
    }
}

实现一个具有拷贝更能的 wrapper:

@propertyWrapper
struct Copying<Value: NSCopying> {
  private var _value: Value
  
  init(wrappedValue value: Value) {
    // Copy the value on initialization.
    self._value = value.copy() as! Value
  }

  var wrappedValue: Value {
    get { return _value }
    set {
      // Copy the value on reassignment.
      _value = newValue.copy() as! Value
    }
  }
}

class Sector: NSObject {
    @Copying var employee: Person
    init( employee candidate: Person ) {
        self.employee = candidate
        super.init()
        assert( self.employee !== candidate )
    }
    override var description: String {
        return "A Sector: [ ( \(employee) ) ]"
    }
}

上面的 employee 用 @Copying 修饰后实现深拷贝

let jack = Person(firstName: "Jack", lastName: "Laven", job: "CEO")
let sector = Sector(employee: jack)
jack.job = "Engineer"
    
print(sector.employee) // Jack Laven, CEO
print(jack) // Jack Laven, Engineer

property wrapper 限制数据范围

还可以自定义 wrapper 约束数据的取值范围,

@propertyWrapper
struct ColorGrade {
    private var number: Int
    init() { self.number = 0 }
    var wrappedValue: Int {
        get { return number }
        set { number = max(0, min(newValue, 255)) }
    }
}

上面定义一个限制颜色分量取值范围的 wrapper ,在 set 方法中,保证赋值不超过 255。我们就可以用 @ColorGrade 修饰其他属性定义一个颜色类型了。

struct ColorType {
    @ColorGrade var red: Int
    @ColorGrade var green: Int
    @ColorGrade var blue: Int
    
    public func showColorInformation() {
        print("red:\(red) green:\(green) blue:\(blue)")
    }
}
var c = ColorType()
c.showColorInformation() // red:0 green:0 blue:0
c.red = 300
c.green = 12
c.blue = 100
c.showColorInformation() // red:255 green:12 blue:100

上面的 ColorGrade 的上限是写死的,struct ColorType 属性的初始值默认都是 0,如果我们想自定义颜色上限和颜色的初始值该怎么做呢?

@propertyWrapper
struct ColorGradeWithMaximum {
    private var maximum: Int
    private var number: Int
    
    init() {
        self.number = 0
        self.maximum = 255
    }
    init(wrappedValue: Int) {
        maximum = 255
        number = min(wrappedValue, maximum)
    }
    init(wrappedValue: Int, maximum: Int) {
        self.maximum = maximum
        number = min(wrappedValue, maximum)
    }
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, maximum) }
    }
}

这样就可以指定上限,在声明属性的时候指定初始值

struct ColorTypeWithMaximum {
    @ColorGradeWithMaximum var red: Int // use  init()
    // @ColorGradeWithMaximum var green: Int = 100 // 和下面的写法一样
    @ColorGradeWithMaximum(wrappedValue: 100) var green: Int // (wrappedValue: 2)
    @ColorGradeWithMaximum(wrappedValue: 90, maximum: 255) var blue: Int // (wrappedValue: 90, maximum:255)
    
    public func showColorInformation() {
        print("red:\(red) green:\(green) blue:\(blue)")
    }
}
  • @ColorGradeWithMaximum var red: Int

是使用 init() 默认初始化方法。

  • @ColorGradeWithMaximum var green: Int = 100@ColorGradeWithMaximum(wrappedValue: 100) var green: Int

上面 2 种写法等价,会调用 (wrappedValue: 2) 的初始化方法。

  • @ColorGradeWithMaximum(wrappedValue: 90, maximum: 255) var blue: Int

这是调用 (wrappedValue: 90, maximum:255) 的初始化方法。

projectedValue

使用 projected value ,可以让 property wrapper 提供更多的功能,例如可以记录一个数据的变化,projected value 的属性名和 wrapped value 一样,不同之处是需要用 $ 访问。

@propertyWrapper
struct Versioned<Value> {
    private var value: Value
    private(set) var projectedValue: [(Date, Value)] = []
    
    var wrappedValue: Value {
        get { value }
        set {
            defer { projectedValue.append((Date(), value)) }
            value = newValue
        }
    }
    
    init(initalizeValue: Value) {
        self.value = initalizeValue
        projectedValue.append((Date(), value))
    }
}

class ExpenseReport {
    enum State { case submitted, received, approved, denied }
    @Versioned(initalizeValue: .submitted) var state: State
}

在 Versioned 中我们用 projectedValue 记录下数据的变化过程,可以用 $state 访问到 projectedValue 查看数据修改的历史:

let report = ExpenseReport()
// print: [(2020-11-08 13:36:07 +0000, SwiftUIDemo.ExpenseReport.State.submitted)]
print(report.$state) // `projectedValue`
report.state = .received
report.state = .approved
// print:[
//  (2020-11-08 13:36:07 +0000, SwiftUIDemo.ExpenseReport.State.submitted),
//  (2020-11-08 13:36:07 +0000, SwiftUIDemo.ExpenseReport.State.received),
//  (2020-11-08 13:36:07 +0000, SwiftUIDemo.ExpenseReport.State.approved)
// ]
print(report.$state)

property wrapper 的局限性

  • 不能在协议里的属性使用。
  • 不能再 enum 里用。
  • wrapper 属性不能定义 getter or setter 方法。
  • 不能再 extension 里用,因为 extension 里面不能有存贮属性。
  • class 里的 wrapper property 不能覆盖其他的 property。
  • wrapper 属性不能是 lazy@NSCopying@NSManagedweak或者unowned.

本文 demo

References