Swift 属性包装器

Property Wrapper, 属性包装器,顾名思义就是包装属性用的。

属性已经有了属性观察器(willSet、didSet)这些方法来帮助我们控制属性的一些行为,为什么还需要属性包装器呢?

比如说,我们业务中经常需要对属性值进行安全检查或一些其它处理时,我们可以利用属性观察器进行处理。但是,当有很多类似的场景时,我们当然不希望相同的处理逻辑需要写多次,这样我们就可以用属性包装器进行包装了。

包装后的属性多了哪些东西呢,一般属性包装器是一个 ​​struct​(也可以是 ​​class​ ),有一个 ​​wrappedValue​ 属性作为被包装属性的实际值。属性包装器可以是一个泛型类型,其泛型参数就是被包装属性的类型,由于属性包装器是一个 ​​struct​/​​class​,所以可以有的东西它也不会少,可以定义额外的属性和方法等来帮助属性额外逻辑的实现。

属性观察器监控和响应属性值的变化,每次属性被设置值的时候都会调用属性观察器,即使新值和当前值相同的时候也不例外。

怎么实现 @propertyWrapper

这里实现一个简单的范围控制属性控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@propertyWrapper
struct RangeValid<Value: Comparable> {
private var _min: Value?
private var _max: Value?
private var _value: Value
private(set) var projectedValue: Bool

init(wrappedValue: Value, min: Value? = nil, max: Value? = nil) {
self._min = min
self._max = max
self._value = wrappedValue

adjust(value: wrappedValue)
}

var wrappedValue: Value {
get {
return self._value
}
set {
update(value: newValue)
}
}

private func adjust(value newValue: Value) {
if let min = _min, newValue < min {
self._value = min
projectedValue = false
} else if let max = _max, newValue > max {
self._value = max
projectedValue = false
} else {
self._value = newValue
projectedValue = true
}
}
}

struct TestStructure {
@RangeValid(min: 1, max: 100) var score: Int = 60
}

let test = TestStructure()
print(test.score) // 60
print(test.$score) // true

test.score = 150
print(test.score) // 100
print(test.$score) // false

test.score = -10
print(test.score) // 0
print(test.$score) // false
  • _min、 _max

这两个值为包装器内的控制值,由使用者随意设置。

  • wrappedValue

包装值,这个名称不能更改,且是必须实现的一个属性。也是我们使用时,外界访问包装器中的值。

  • projectedValue

映射值,在被修饰的属性前加上 $ 符号,调用的就是 projectedValue 的 get 方法。这个名称也不能更改。

属性包装器可以返回任何类型的值作为它的被映射值。

几个例子

UserDefault

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@propertyWrapper
struct UserDefaultWrapper<Value: UserDefaultsSerializable> {
private let _userDefaults: UserDefaults

private var key: String

private var defaultValue: Value?

init(key: String, defaultValue: Value? = nil, userDefault: UserDefaults = .standard) {}
self.key = key
self.defaultValue = defaultValue
self._userDefaults = userDefault

// 对key所对应的值进行初始化(已有值则跳过,没有则进行初始化)
userDefaults.registerDefault(value: defaultValue, key: key)
}

var wrappedValue: Value? {
get {
return _userDefaults.object(forKey: key) as? Value ?? defaultValue
}
set {
if (newValue == nil) {
_userDefaults.removeObject(forKey: key)
} else {
_userDefaults.setValue(newValue, forKey: key)
}
_userDefaults.synchronize()
}
}
}

去除字符串中的空格

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@propertyWrapper
struct StringWrapper {
private var value: String?

var wrappedValue: String {
get {
return self.value ?? ""
}
set {
self.value = adjust(value: newValue)
}
}

private func adjust(value newValue: String) -> String {
return newValue.trimmingCharacters(in: .whitespacesAndNewlines)
}
}


struct SomeStructure {
@StringWrapper var name: String
}

var s = SomeStructure()
s.name = " 哈哈哈哈 "
print(s.name) // 哈哈哈哈

总结

  • @propertyWrapper 的本质就是包装修饰属性的 set get 方法。
  • @propertyWrapper 可以有效降低代码重复率。

一些限制

  • 带有包装器的属性不能在子类中覆盖。
  • 具有包装器的属性不能是lazy,@NSCopying,@NSManaged,weak或unowned。
  • 具有包装器的属性不能具有自定义的 set 或 get 方法。
  • wrappedValue,init(wrappedValue :) 和 projectedValue 必须具有与包装类型本身相同的访问控制级别。
  • 不能在协议或扩展中声明带有包装器的属性。