從SwiftUI的@State來看看Property Wrapper

語言: CN / TW / HK

本文緣起於一個晚上,看着SwiftUI突然想象,@State怎麼這麼能幹,我就想看看它的羈絆,順便向大家呼喚一個點贊~

@State是什麼?

我想每一個學習SwiftUI的人,肯定會看到這樣的代碼:

```swift struct PlayButton: View { @State private var isPlaying: Bool = false

var body: some View {
    Button(isPlaying ? "Pause" : "Play") {
        isPlaying.toggle()
    }
}

} ```

這其中有一個和以往不太一樣的東西:@State,在最開始的時候,我以為它是類似於@objc 之類的這種關鍵字,結果發現不然,因為我可以直接從Xcode中跳轉查看@State對外暴露的API:

```swift @frozon @propertyWrapper public struct State: DynamicProperty { public init(wrapperdValue value: Value){...}

        public init(initialValue value: Value){...}

        public var wrappedValue: Value { get nonmutating set }

        public var projectedValue: Binding<Value> { get }

} ```

@PropertyWrapper是什麼?

看到這裏,我想大家都意識到了@State 只是表現形式,真正關鍵的是@propertyWrapper 也就是屬性包裝器,在第一次遇到這種概念的時候,難免有些不知所措,但是這個特性早已經在Swift的官方文檔中有過介紹了:

A property wrapper adds a layer of separation between code that manages how a property is stored and the code that defines a property.(屬性包裝器在管理屬性如何存儲和定義屬性的代碼之間添加了一個隔離層)

也就是説這個隔離層做了什麼就是屬性包裝器的真正作用!可是它具體做了什麼呢?我想通過一個案例來聊一聊,我們都使用過UserDefaults 來存儲過數據,通常我會這樣使用:

```swift extension UserDefaults { enum Keys { static let didLogIn = "didLogIn" }

static var didLogIn: Bool {
    get {
        return UserDefaults.standard.object(forKey: UserDefaults.Keys.didLogIn) as? Bool ?? false
    }
    set {
        UserDefaults.standard.set(newValue, forKey: UserDefaults.Keys.didLogIn)
    }
}

} ```

值得慶幸的是這裏只有一個Key: didLogIn 但是往往在項目中,我們可能會用到多個Key來保存不同的數據,不可否認,重複的代碼會越來越多,這和Swift簡潔優雅的語言特性是不符合的,那麼有沒有一個更好的方式呢?有,那就是@PropertyWrapper ,它可以封裝屬性內部的行為,再加上範型的使用,消除重複的邏輯代碼,提高代碼的可讀性,降低代碼量。具體來説如下:

```swift @propertyWrapper public struct UserDefault { var key: String var defaultValue: Value let container = UserDefaults.standard

public var wrappedValue: Value {
    get {
        container.object(forKey: key) as? Value ?? defaultValue
    }
    set {
        container.set(newValue, forKey: key)
    }
}

}

extension UserDefaults { @UserDefault(key: UserDefaults.Keys.didLogIn, defaultValue: false) static var didLogIn: Bool } ```

通過上述代碼,我們可以定義一個UserDefault ,用它來封裝對於屬性的管理,使用的時候直接在屬性前添加@UserDefault 即可,非常的具備可讀性。那我對@PropertyWrapper 做的定義如下:屬性包裝器將對屬性的管理行為做了封裝,具體來説get/set/wiiSet/didSet等行為,並提供了複用的機制。

如何使用@PropertyWrapper?

那麼接下來我們將聊一聊@PropertyWrapper是如何使用的,如果是已經有基礎的朋友,看到這裏就可以點個贊👍 然後去看我其他的文章了,如果沒有相關基礎,那我們接着往下走。我將從Apple屬性包裝器的文檔中的案例來展開:

如何定義?

  • 需要在類型前加上@propertyWrapper,類型可以是結構體、枚舉或者類
  • 類型一定要定義一個wrapperValue屬性

就只有這兩點規則,比如我們實現一個:

swift **@propertyWrapper struct EmptyProperty<Value> { var wrappedValue: Value }**

如何初始化?

定義是非常簡單的,初始化也很簡單,上述例子中@propertyWrapper修飾的本身就是一個類型,我們之所以不添加初始化方法,是因為struct幫我們自動生成來初始化方法,實際上我們最好是自己實現初始化方法。

```swift @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) {
    maximum = 12
    number = min(wrappedValue, maximum)
}
init(wrappedValue: Int, maximum: Int) {
    self.maximum = maximum
    number = min(wrappedValue, maximum)
}

} ```

在上述定義中實現了三種初始化方法,所以我們使用的時候也可以使用三種初始化方法:

```swift struct A { @SmallNumber var num1: Int

@SmallNumber var num2 = 13
@SmallNumber(wrappedValue: 13) var num3

@SmallNumber(wrappedValue: 13, maximum: 15) var num4
@SmallNumber(maximum: 15) var num5 = 13

} ```

  • struct A中第一種方式對應定義中的第一種初始化方法:init()
  • struct A中第二種方式和第三種方式等價,都對應定義中的第二種初始化方法:init(wrappedValue: Int)
  • struct A中第四種方式和第五種方式等價,都對於定義中的第三種初始化方法:init(wrappedValue: Int, maximum: Int)

我們能看出,直接對該屬性賦值就等於是對屬性包裝器中的wrappedValue 進行了賦值。

換言之,我們定義的num1屬性就是屬性包裝器中的wrappedValue屬性,那麼如何獲得屬性包裝器本身呢?很簡單使用_num1 前面加上下劃線即可:

```swift _num1.wrappedValue 等價於 num1

_num1 就是屬性包裝器類型 (即為SmallNumber類型) ```

如何使用投射屬性?

除了wrappedValue ,屬性包裝器還可以通過定義projectedValue暴露出其他功能,即投射任何屬性(包括自身),而這個被呈現值需要以 $ 符號來開頭,除此之外被呈現值的名稱和被包裝值是一樣的。比如説在上面SmallNumber的例子中投射一個需求:是否在存儲之前調整了新值。

```swift @propertyWrapper struct SmallNumber { private var number: Int // 被投射值 private(set) var projectedValue: Bool

var wrappedValue: Int {
    get { return number }
    set {
        if newValue > 12 {
            number = 12
            projectedValue = true
        } else {
            number = newValue
            projectedValue = false
        }
    }
}

init() {
    self.number = 0
    self.projectedValue = false
}

} struct SomeStructure { @SmallNumber var someNumber: Int } var someStructure = SomeStructure()

someStructure.someNumber = 4 print(someStructure.$someNumber) // 打印 "false"

someStructure.someNumber = 55 print(someStructure.$someNumber) // 打印 "true" ```

也就是説我們可以得到如下的對應關係:

```swift _屬性名稱 = PropertyWrapper類型 (如:_num1 的類型是SmallNumber類型)

$屬性名稱 = _屬性名稱.projectedValue (如:$someNumerber是projectedValue的值)

屬性名稱 = _屬性名稱.wrapperedValue (如:_num1 的類型是SmallNumber類型中的wrapperedValue) ```

聲明時的限制

  • 被修飾的屬性不能是lazy、weak、或者unowned的
  • PropertyWrapper 類型本身必須和wrappedValue、projectedValue必須有同樣的access control level

@PropertyWrapper的更多實例

雖然我們已經知道了該特性的用法,但是千萬不能從字面上理解,比如每一次使用didSet時都採用PropertyWrapper,而是要從具體的使用場景出發,發現更通用的需求,最好可以結合範型,以應對更多的類型場景。以下我拿兩個實際場景來舉例:

場景1: 懶加載

```swift // 懶加載 @propertyWrapper public struct LateInitialized { public var wrappedValue: Value { get { guard let value = storage else { fatalError("value is wrong") } return value }

    set {
        storage = newValue
    }
}

public init(_ value: Value) {
    storage = value
}

private(set) var storage: Value?

public var projectedValue: Self { self }

}

struct MyTypeOne { @LateInitialized var text: String } ```

場景2: 防禦性拷貝

```swift @propertyWrapper public struct DefensiveCopying { private var storage: Value

public var wrappedValue: Value {
    get { return storage }
    set {
        storage = newValue.copy() as! Value
    }
}

public init(wrappedValue: Value) {
    storage = wrappedValue.copy() as! Value
}

} ```

總結

為什麼我會聊這個呢?因為最近在看Swift自定義DSL,而弄懂這個,需要一些前置條件,所以本文就聊了聊關於PropertyWrapper的內容,後續會聊到Result Builder以及Key Path Look Up,以及最後的DSL。

參考

1、Swift官方文檔

2、SwiftGG文檔

3、WWDC19 Session415 《Modern Swift API Design》