🔰property wrapper
把真正的資料包裹起來,只開放一個或兩個公開窗口的結構。
Swift ⟩ type ⟩ property ⟩ wrapper
A property wrapper is a simple way to apply a common pattern of behavior to a property, it's a way of annotating the property that defines how it will be stored or computed on reading.
- built-in property wrappers 
- Property Wrappers - Swift 
- Swift 5.1 features. 
- @State, @Environment are property wrappers. 
- Namespace used by Matched Geometry Effect is a property wrapper. 
- GestureState is used to track Gestures state (e.g. Long Press) 
- about Property Wrapper's initializers - StackOverflow 
緣起
如果我們要設計一個「學生」的結構,然後有「分數」這個屬性,但規定不管我們如何輸入這個分數,這個分數都只能介於 0 ~ 100 之間。例如:
我們輸入的分數
學生實際的分數
說明
-200
0
最低只能 0 分。
60
60
如果是正常的分數,就不改變。
300
100
最高只能 100 分。
如果輸入程式碼的話,要長這樣:
var student = Student()
student.grade = 500
print(student.grade)        // 結果是:100
student.grade = 60
print(student.grade)        // 結果是:60
student.grade = -20
print(student.grade)        // 結果是:0這時要如何設計這個結構 (struct) 呢?首先,嘗試下面的程式碼:
struct Student {
    // 1. 使用 private var 儲存分數,不讓外部存取。
    private var _grade: Int = 0
    // 2. 用 computed property 來控管分數的輸入輸出 (get/set)。
    var grade: Int {
        // 2.1 (get) 輸出不控管,直接回傳內部儲存的分數。
        get { _grade }
        // 2.2 (set) 輸入要控管,只儲存 0 ~ 100 之間的分數
        set {
            _grade =  
                newValue <   0 ?   0 :
                newValue > 100 ? 100 :
                newValue
        }
    }
}只要這樣設計,加入一個「私密變數」(private var)、再一個「計算屬性」(computed property),就可以達到我們原先的要求:「可以控管學生的分數始終在 0 ~ 100 之間」。
任務完美達成,還有問題嗎❓
這樣做雖然沒什麼問題,但問題在於:在程式設計中,類似這樣的模式 (pattern) 實在太常出現,例如:「分數在 0 ~ 100 之間」、「評分在 0 ~ 10 之間」、「pH 值在 -1 到 16 之間」、「顏色值在 0 ~ 255 之間」等等,這些一再出現的類似模式,如果每次遇到的時候,都要重新寫一次,那就違反了程式設計的 DRY 綱領:「 Don't Repeat Yourself❗️」,而這也就是為什麼 Swift 要在 5.1 的版本中加入 Property Wrapper 的功能了,Property Wrapper 可以讓我們只設計一次,就解決上面所有的類似模式,不需每次遇到時就重寫一次。
為何叫 Property Wrapper❓
你只要仔細觀察上面「控管學生分數」的例子就會發現:真正的分數是被藏在私密變數 _grade 中,外界無法直接存取這個變數,而是必須透過計算屬性(computed property) grade 來存取它,換句話說,真正的分數等於是被包起來(wrapped)了,而計算屬性 grade 某種程度說來,就是包住它的人(wrapper),而這也就是為什麼 Swift 要將這個新功能稱為:「Property Wrapper」的原因了。
下面我來說明:如何用 Property Wrapper 來達成上面所說的「控管學生的分數」。
設計第一個 Property Wrapper
首先,我們必須先設計一個可以「控管學生的分數」的結構(struct),設計的方式跟上面的計算屬性 grade 很類似:👉 程式碼:paiza.io
// ⭐️ 1. 必須用 "@propertyWrapper" 指明:
//       這個 struct 是用來「控管分數」的,
//       也就是用來包裹分數屬性的 property wrapper。
@propertyWrapper
// 2. 名字可自訂,但因為要「控管分數」,所以取名 "Grade"
struct Grade {
    // 3. 真正的分數藏這裡(變數名稱可自訂)
    private var _grade = 0
    
    // ⭐️ 4. 管控與外界聯繫的「窗口」:
    //       一定要稱為 "wrappedValue",這是內定關鍵字❗️
    var wrappedValue: Int {
        get { _grade }
        set {
            _grade = 
                newValue <   0 ?   0 :
                newValue > 100 ? 100 :
                newValue
        }
    }
}
// 現在控管分數的屬性,只要一行就可以搞定❗️
struct Student {
    // ⭐️ 5. 使用 @propertyWrapper Grade 來控管這個分數
    //       使用時 Grade 前面要加 "@"❗️
    @Grade var grade: Int
}
var student = Student()
student.grade = 500
print(student.grade)        // 結果是:100
student.grade = 60
print(student.grade)        // 結果是:60
student.grade = -20
print(student.grade)        // 結果是:0這樣設計,有何好處❓
這樣設計的好處是:如果我們有很多科的分數都要登記,例如:國文、英文、數學等,在沒有 property wrapper 之前,我們必須為每一科的分數寫一個計算屬性(computed property),才能確保每個分數都在 0 ~ 100 之間。
但有了 property wrapper 之後就不需要了,因為我們已經將「確保分數都在 0 ~ 100 之間」這樣的機制寫在 @propertyWrapper Grade 之中,所以只要在每科分數的變數前面加上 @Grade ,註明我們要用相同的機制來控管這些分數就可以,如下:
struct Student {
    // ⭐️ 5. 使用 @propertyWrapper Grade 來控管這些分數
    //       注意:使用時 "Grade" 前面要加 "@"❗️
    @Grade var math   : Int
    @Grade var english: Int
    @Grade var chinese: Int
}
var student = Student()
student.math = 500
print(student.math)           // 結果是:100
student.english = 60
print(student.english)        // 結果是:60
student.chinese = -20
print(student.chinese)        // 結果是:0留下的問題
- 當我們用 - @propertyWrapper Grade(後面開始都簡稱為- @Grade) 來控管一個分數屬性時,Swift 到底背後做了什麼魔法❓
- 當我們設計了 - @Grade,雖然可以控管分數,卻也只能控管「0 ~ 100」的分數。如果我們要控管其他範圍的數字,如:pH 值、顏色值等等,又該如何❓
這些問題且待我們有空時,繼續分解。🚧

⚠️ 以下內容尚未整理,請自行斟酌觀看❗️
一個 property wrapper 就是把真正的資料 (圖中的 _value) 包裹起來,只開放一個或兩個公開的窗口 (圖中的 wrappedValue 與 projectedValue) 來存取這個資料的結構 (通常是 struct,但也可以是 class, enum 等其他型別)。

目前只能存活在別的結構裡面,當作別人的屬性,還不能獨立存在,所以稱為 Property Wrapper。
以一當三

以上圖為例,我們為 Person 這個結構添加了一個 property wrapper - @Wrapped var name,但實際上,Swift compiler 會在背後自動產生三個屬性 (如果我們有定義 projectedValue 的話):
- name:這是公開屬性,直接與 wrappedValue 對接,用 - person.name就等同於存取 property wrapper 的 wrappedValue。
- $name:也是公開屬性,直接與 projectedValue 對接,用 - person.$name等同於存取 property wrapper 的 projectedValue,但此值是有宣告才有,不是每個 property wrapper 都必備。
- _name:這是 Person 的私密屬性,代表 property wrapper 本身,只能在 Person 內部使用,在外部並不能用 - person._name來存取。
重點
- 只能用 var 宣告,不能用 let。 
- 可以在別的 property wrapper 裡面當屬性來用,例如:@UnitInterval 
可以用來做什麼❓
- 自動編碼 (Codable):👉 
目前不支援
函數內區域變數
func liveView() -> some View {
    
    // ❌ property wrappers are not yet supported on local properties
    @State var text = ""
    
    Text("Hello World")
}廣域變數
// ❌ property wrappers are not yet supported in top-level code
@State var tmp = ""範例
@propertyWrapper
struct SmallNumber {
    
    // ⭐️ Property Wrapper 背後掌管的變數
    private var _max  : Int
    private var _value: Int
    
    // ⭐️ 要存取背後變數的統一通道 (wrappedValue)
    var wrappedValue: Int {
        get { return _value }
        set { _value = min(newValue, _max) }
    }
    
    /*
     * ⚠️ 注意:
     *    Swift 5.1 需要另外定義兩個 .init
     *    - .init()
     *    - .init(wrappedValue:) 
     */
    init(){
        self._max   = 12
        self._value = 0
    }
    init(wrappedValue: Int) {
        self._max   = 12
        self._value = min(wrappedValue, 12)
    }
    
    /*
     *    Swift 5.2 則不需要,只要下面這個就可以。
     */
    init(wrappedValue: Int = 0, max: Int = 12) {
        self._max   = max
        self._value = min(wrappedValue, max)
    }
}// ⭐️ Property Wrapper 不同的宣告方式,
//    背後會使用不同的 initializer。
struct SmallNumbers {
    // ⭐️ 1. 前面沒有參數(max:),後面沒有(=):
    @SmallNumber         var n1: Int      // init()
    // ⭐️ 2. 後面有(=):
    @SmallNumber         var n2: Int = 1  // .init(wrappedValue:)
    // ⭐️ 3. 後面有(=),前面也有參數(max:):
    @SmallNumber(max: 9) var n3: Int = 10 // .init(wrappedValue:max:)
    // ⭐️ 4. 前面有參數(max:),後面沒有(=):
    @SmallNumber(max: 9) var n4: Int      // .init(wrappedValue:max:)
    // ⭐️ 5. 直接使用 .init(wrappedValue:max:) == 3.
    @SmallNumber(wrappedValue: 8, max: 5) var n5: Int   // .init(wrappedValue:max:)
}
var s = SmallNumbers()
print(s.n1, s.n2, s.n3, s.n4, s.n5)   // 0 1 9 0 5
// ❌ top-level property wrappers not supported (yet)
//  @SmallNumber var a: Int = 3- Property Wrappers - Swift 
Projected Value
#todo
Last updated
Was this helpful?