It’s now or never

IT系の技術ブログです。気になったこと、勉強したことを備忘録的にまとめて行きます。

【Swift】【SwiftUI】Property wrappers に入門してみた

概要

Swift 5.1から「Property Wrappers」という機能が導入されました。

これはプロパティの読み込み/書き込みをカプセル化するためのデータ構造です。

SwiftUIの導入により、Swiftには様々な機能が追加されましたが、このProperty Wrappersはその中でも特に重要な根本機能です。
まずはこの基本を理解して、その他の新機能についても深堀りできればと思います。

実行環境

  • Swift 5.1

実装例

簡単な例でみてみましょう。

struct Test {
    @DefaultValue(defaultValue: 10)
    var num: Int
}

@DefaultValue がプロパティラッパです。
このようにプロパティの前に 「@xxx」のように記述することでプロパティラッパを付与することができます。

この例の「@DefaultValue」では、ある数値プロパティに対して予めデフォルト値を設定しておくことができます。
仕様としては、プロパティに0が設定された時(値が設定されていない時)には、予め設定されているデフォルト値が設定されるようにします。

参照例は次のとおりです。

var t = Test()
print(t.num) // => 10(デフォルト値)
t.num = 20
print(t.num) // => 20
t.num = 0
print(t.num) // => 10(デフォルト値)

これを実現するためのコードは次のようになります。

@propertyWrapper
struct DefaultValue<Value> where Value: Numeric {
    private var value: Value
    private var defaultValue: Value
    
    // デフォルトのイニシャライザ
    init(wrappedValue v: Value) {
        value = v
        self.defaultValue = 0
    }
    
    // カスタムイニシャライザ(デフォルト値の設定)
    init(defaultValue: Value) {
        value = defaultValue
        self.defaultValue = defaultValue
    }
    
    var wrappedValue: Value {
        get { value }
        set {
            // 値が0の場合はデフォルト値を設定する
            if newValue == 0 {
                value = defaultValue
            } else {
                value = newValue
            }
        }
    }
}

プロパティラッパを定義するには、構造体、列挙型、クラスの定義の前に「@propertyWrapper」を置きます。
@propertyWrapperとして定義すると、計算型プロパティ「wrappedValue」の実装が必須になります。
initを必須ではありませんが、多くの場合は初期値設定として実装することが多いかと思います。
また、プロパティラッパは様々な型のプロパティへ付与されるためジェネリクスを利用して方を定義する場合が多いです。
(今回は数値を例にしているのでNumericに適用する型を定義しています)

プロパティラッパをあるプロパティに設定した場合、そのプロパティへの参照は、この「wrappedValue」へのアクセスへ仲介されます。

var t = Test()
// 動作時にはDefaultValue構造体のwrappedValueのsetterが呼び出される
t.num = 100

このようにして、値への操作をカプセル化することが可能です。

今回の例では、値をセットする時に値が0であればデフォルト値が設定されるように「wrappedValue」のsetterを実装しています。

ストレージプロパティ

プロパティラッパを付与したプロパティへのアクセスは、「wrrappedValue」を仲介されますが、プロパティラッパの実装領域へ直接アクセスすることも可能です。

struct Test {
    @DefaultValue(defaultValue: 10)
    var num: Int
}

このプロパティ num は、 _num とすることでプロパティラッパの構造へアクセスすることができます。

struct Test {
    @DefaultValue(defaultValue: 10)
    var num: Int
    
    func showNum() {
        // DefaultValueのプロパティwrappedValueへアクセスできる 
        print(_num.wrappedValue)
    }
}

var t = Test()
t.showNum() // => 10

このようにプロパティラッパの実装領域へ直接アクセスできるプロパティ(_をつけたプロパティ)を ストレージプロパティ といいます。
ストレージプロパティの可視範囲はprivateになっており、構造体の外からはアクセスできません。

射影値(projected value)

ストレージプロパティは、privateのため外部参照がでませんが、目的によっては外部から意図的にアクセスしたいケースが存在します。
そのためにプロパティラッパには 射影値(projected value) という計算型プロパティが用意されています。
この射影値を使うとプロパティを定義した構造体やクラスの外部からプロパティ内部の値へアクセスすることが可能です。

@propertyWrapper
struct DefaultValue<Value> where Value: Numeric {
    ・・・ 
    var projectedValue: Value {
        get { value }
        set {
            if newValue == 0 {
                value = defaultValue
            } else {
                value = newValue
            }
        }
    }
}

このように @propertyWrapper を定義した構造体やクラスに projectedValue という計算型プロパティを実装します。
こうすることで、プロパティラッパを付与した構造体やクラスからは、$<プロパティ名> でのアクセスが可能となります。

var t = Test()
t.$num = 20
print(t.$num) // => 20

$ をプロパティ名の先頭につけ $num としてアクセスすることで「projectedValue」プロパティを仲介することができます。

まとめ

「Property Wrappers」を使うことにより、様々なプロパティの値をカプセル化して処理することが可能になりました。
また射影値を使うことでクラスや構造体から外部の値を参照や更新することが可能となりSwiftUIの機能の根幹ともなっています。
一方で初見では理解が難しい機能だとも感じており、開発を円滑に進めるためにも少しずつ根本理解していく必要がありそうです。