Swift – PropertyWrapper

一、知識點

Property Wrapper,即屬性包裝器,其作用是將屬性的 定義代碼 與屬性的存儲方式代碼 進行分離,抽取的管理的存儲代碼只需要編寫一次,即可將功能應用於其它屬性上。

1、基礎用法

功能需求:確保值始終小於或等於12

這里我們直接使用 property wrapper 進行封裝演示

@propertyWrapper
struct TwelveOrLess {
private var number: Int
// wrappedValue變量的名字是固定的
var wrappedValue: Int {
get { return number }
set { number = min(newValue, 12) }
}
init() {
self.number = 0
}
}
struct SmallRectangle {
@TwelveOrLess var height: Int
@TwelveOrLess var width: Int
}
var rectangle = SmallRectangle()
print(rectangle.height) // 0
rectangle.height = 10
print(rectangle.height) // 10
rectangle.height = 24
print(rectangle.height) // 12

這里可以注意到,在創建 SmallRectangle 實例時,並不需要初始化 height 和 width

原因: 被 property wrapper 聲明的屬性,實際上在存儲時的類型是 TwelveOrLess,只不過編譯器施了一些魔法,讓它對外暴露的類型依然是被包裝的原來的類型。

上面的 SmallRectangle 結構體,等同於下方這種寫法

struct SmallRectangle {
private var _height = TwelveOrLess()
private var _width = TwelveOrLess()
var height: Int {
get { return _height.wrappedValue }
set { _height.wrappedValue = newValue }
}
var width: Int {
get { return _width.wrappedValue }
set { _width.wrappedValue = newValue }
}
}

2、設置初始值

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

使用了 @SmallNumber 但沒有指定初始化值

struct ZeroRectangle {
@SmallNumber var height: Int
@SmallNumber var width: Int
}
var zeroRectangle = ZeroRectangle()
print(zeroRectangle.height, zeroRectangle.width) // 0 0

使用了 @SmallNumber ,並指定初始化值

這里會調用 init(wrappedValue:) 方法

struct UnitRectangle {
@SmallNumber var height: Int = 1
@SmallNumber var width: Int = 1
}
var unitRectangle = UnitRectangle()
print(unitRectangle.height, unitRectangle.width) // 1 1

使用@SmallNumber,並傳參進行初始化

這里會調用 init(wrappedValue:maximum:) 方法

struct NarrowRectangle {
// 報錯:Extra argument 'wrappedValue' in call
// @SmallNumber(wrappedValue: 2, maximum: 5) var height: Int = 1
// 這種初始化是可以的,調用 init(wrappedValue:maximum:) 方法
// @SmallNumber(maximum: 9) var height: Int = 2
@SmallNumber(wrappedValue: 2, maximum: 5) var height: Int
@SmallNumber(wrappedValue: 3, maximum: 4) var width: Int
}
var narrowRectangle = NarrowRectangle()
print(narrowRectangle.height, narrowRectangle.width) // 2 3
narrowRectangle.height = 100
narrowRectangle.width = 100
print(narrowRectangle.height, narrowRectangle.width) // 5 4

3、projectedValue

projectedValue為 property wrapper 提供了額外的功能(如:標誌某個狀態,或者記錄 property wrapper 內部的變化等)

兩者都是通過實例的屬性名進行訪問,唯一不同的地方在於,projectedValue 需要在屬性名前加上 $ 才可以訪問

  • wrappedValue: 實例.屬性名
  • projectedValue: 實例.$屬性名

下面的代碼將一個 projectedValue 屬性添加到 SmallNumber 結構中,以在存儲該新值之前跟蹤該屬性包裝器是否調整了該屬性的新值。

@propertyWrapper
struct SmallNumber1 {
private var number: Int
var projectedValue: Bool
init() {
self.number = 0
self.projectedValue = false
}
var wrappedValue: Int {
get { return number }
set {
if newValue > 12 {
number = 12
projectedValue = true
} else {
number = newValue
projectedValue = false
}
}
}
}
struct SomeStructure {
@SmallNumber1 var someNumber: Int
}
var someStructure = SomeStructure()
someStructure.someNumber = 4
print(someStructure.$someNumber) // false
someStructure.someNumber = 55
print(someStructure.$someNumber) // true

這里的 someStructure.$someNumber 訪問的是 projectedValue

4、使用限制

  • 不能在協議里的屬性使用
protocol SomeProtocol {
// Property 'sp' declared inside a protocol cannot have a wrapper
@SmallNumber1 var sp: Bool { get set }
}
  • 不能在 extension 內使用
extension SomeStructure {
// Extensions must not contain stored properties
@SmallNumber1 var someProperty2: Int
}
  • 不能在 enum 內使用
enum SomeEnum {
// Property wrapper attribute 'SmallNumber1' can only be applied to a property
@SmallNumber1 case a
case b
}
  • class 里的 wrapper property 不能覆蓋其他的 property
class AClass {
@SmallNumber1 var aProperty: Int
}
class BClass: AClass {
// Cannot override with a stored property 'aProperty'
override var aProperty: Int = 1
}
  • wrapper 屬性不能定義 getter 或 setter 方法
struct SomeStructure2 {
// Property wrapper cannot be applied to a computed property
@SmallNumber1 var someNumber: Int {
get {
return 0
}
}
}
  • wrapper 屬性不能被 lazy、 @NSCopying、 @NSManaged、 weak、 或者 unowned 修飾

二、實際應用

Foil — 對 UserDefaults 進行了輕量級的屬性包裝第三方庫

這部分我們主要簡單的看下該第三方庫的核心實現與使用

1、使用

  • 聲明
// 聲明使用的key為flagEnabled,默認值為true
@WrappedDefault(key: "flagEnabled", defaultValue: true)
var flagEnabled: Bool
// 聲明使用的key為timestamp
@WrappedDefaultOptional(key: "timestamp")
var timestamp: Date?
  • 獲取
// 獲取變量在UserDefault中對應存儲的值
self.flagEnabled
self.timestamp
  • 賦值
// 設置UserDefault中對應存儲的值
self.flagEnabled = false
self.timestamp = Date()

2、核心代碼

WrappedDefault.swift 文件

@propertyWrapper
public struct WrappedDefault<T: UserDefaultsSerializable> {
private let _userDefaults: UserDefaults
/// 使用UserDefaults是所使用的key
public let key: String
/// 從UserDefaults中獲取到的值
public var wrappedValue: T {
get {
self._userDefaults.fetch(self.key)
}
set {
self._userDefaults.save(newValue, for: self.key)
}
}
// 初始化方法
public init(
keyName: String,
defaultValue: T,
userDefaults: UserDefaults = .standard
) {
self.key = keyName
self._userDefaults = userDefaults
// 對key所對應的值進行初始化(已有值則跳過,沒有則進行初始化)
userDefaults.registerDefault(value: defaultValue, key: keyName)
}
}

WrappedDefaultOptional.swift 文件

@propertyWrapper
public struct WrappedDefaultOptional<T: UserDefaultsSerializable> {
private let _userDefaults: UserDefaults
public let key: String
/// 從UserDefaults中獲取到的值,無則返回nil
public var wrappedValue: T? {
get {
self._userDefaults.fetchOptional(self.key)
}
set {
if let newValue = newValue {
// 更新值
self._userDefaults.save(newValue, for: self.key)
} else {
// 刪除值
self._userDefaults.delete(for: self.key)
}
}
}
public init(keyName: String,
userDefaults: UserDefaults = .standard) {
self.key = keyName
self._userDefaults = userDefaults
}
}

來源:kknewsSwift – PropertyWrapper