【Swift 5.1】Property Wrappersとは?

Property Wrappers とは、簡単に言えば「プロパティに関する制御をテンプレ化できる仕組み」です。

厳密に言えば、「プロパティの定義と制御の間に1つレイヤーを挟むための仕組み」です。

Swift 5.1で実装されました。

Property Wrappersを使わないこれまでの書き方

例えば、学校のテスト結果(国語、数学、理科、社会、英語)を入れる構造体を定義したとして、点数を0〜100までの範囲に限定したい場合、以下のように書いてきました。

struct TestResult {
    /// 国語
    private var _japanese: Int = 0
    var japanese: Int {
        get { return _japanese }
        set { _japanese = min(100, max(0, newValue)) }
    }
    
    /// 数学
    private var _math: Int = 0
    var math: Int {
        get { return _math }
        set { _math = min(100, max(0, newValue)) }
    }
    
    /// 理科
    private var _science: Int = 0
    var science: Int {
        get { return _science }
        set { _science = min(100, max(0, newValue)) }
    }
    
    /// 社会
    private var _social: Int = 0
    var social: Int {
        get { return _social }
        set { _social = min(100, max(0, newValue)) }
    }
    
    /// 英語
    private var _english: Int = 0
    var english: Int {
        get { return _english }
        set { _english = min(100, max(0, newValue)) }
    }
}

各教科それぞれにゲッターとセッターを書いて、さらにはStored Propertyとしての変数も切ってと、とても面倒でした。

Property Wrappersを使った書き方

この面倒なプロパティ定義・制御を、Property Wrappersを使うことでテンプレ化することができます。

方法として、まずは @propertyWrapper を付けてstruct(またはclass, enum)を定義します↓

@propertyWrapper
struct Score {
    
    private var value: Int

    init() {
        value = 0
    }
    
    var wrappedValue: Int {
        get { return value }
        set { value = min(100, max(0, newValue)) }
    }
}

wrappedValue プロパティの定義は必須で、これがProperty Wrappersの肝となります。

これだけでテンプレの定義はできたので、先ほどのTestResultを書き換えてみます↓

struct TestResult {
    /// 国語
    @Score var japanese: Int
    
    /// 数学
    @Score var math: Int
    
    /// 理科
    @Score var science: Int
    
    /// 社会
    @Score var social: Int
    
    /// 英語
    @Score var english: Int
}

各プロパティの頭に @ 付きで定義したものを書くだけ。

実行してみても意図したとおりに動いています。

var result = TestResult()
result.japanese = -80
print(result.japanese) // -> 0

result.math = 120
print(result.math) // -> 100

result.science = 70
print(result.science) // -> 70

result.social = 90
print(result.social) // -> 90

result.english = 150
print(result.english) // -> 100

素晴らしい。

初期値を渡す

プロパティ宣言時に初期値を渡すこともできます。

例えば、各教科一律で0〜100点にするのではなく、教科ごとに範囲を設けたい場合、先程のScore構造体を以下のように変更します。

@propertyWrapper
struct Score {
    
    private var value: Int
    private var limit: CountableClosedRange<Int>
    
    init() {
        self.value = 0
        self.limit = 0...100
    }
    
    init(limit: CountableClosedRange<Int>) {
        self.value = 0
        self.limit = limit
    }

    var wrappedValue: Int {
        get { return value }
        set { value = min(limit.upperBound, max(limit.lowerBound, newValue)) }
    }
}

limitプロパティを追加し、それを渡す用のイニシャライザも追加しました。

これでTestResultも書き換えてみます↓

struct TestResult {
    /// 国語
    @Score(limit: (-50)...150) var japanese: Int
    
    /// 数学
    @Score(limit: 0...150) var math: Int
    
    /// 理科
    @Score(limit: 0...50) var science: Int
    
    /// 社会
    @Score(limit: 100...1000) var social: Int
    
    /// 英語
    @Score var english: Int
}

まあちょっとあり得ない範囲設定になっていますが。。

動かしてみても、

var result = TestResult()
result.japanese = -80
print(result.japanese) // -> -50

result.math = 120
print(result.math) // -> 120

result.science = 70
print(result.science) // -> 50

result.social = 90
print(result.social) // -> 100

result.english = 150
print(result.english) // -> 100

いい感じです。

参考リンク

・より詳しい情報はこちら
Swift.org:
https://docs.swift.org/swift-book/LanguageGuide/Properties.html#ID617

・参考
Qiita「Swift5.1 PropetyWrappersを使ったUserDefaultsの例」:
https://qiita.com/kazy_dev/items/b0ef4775d3acb96c200d