【Swift】Property Wrapper入門編:基本から応用まで

Swift

こんにちは。新人プログラマーの岩本です。

今回はプロパティラッパーについて調べたことを、自分なりにまとめたいと思います。

この記事を読むことで普段書いているコードの解像度が上がると思います。自分もプロパティラッパーを勉強中に、「そういうことだったのか!」となることが度々ありました。

ぜひ最後までご覧ください。

この記事は新卒エンジニアが執筆しています。
そのため内容に間違いや不備がある場合があります。
もし間違いを発見しましたら、どんどん指摘していただけると幸いです。

実装環境

  • 言語…Swift 5.8.1
  • OS… macOS Sonoma 14.1.1
  • アプリ… Xcode 14.3.1

【基本編】Property Wrapperとは?

プロパティラッパーとは、一言で言うと「プロパティにカスタムな振る舞いや機能を提供するための仕組み」です。

例えるなら「🥘料理」です。プロパティは「🧅食材」で、プロパティラッパーは与えられた食材をどう調理するかが記述された「📝レシピ」です。

定義するにはクラス名や構造名の前に @propertyWrapper とつけます。

SwiftUIを使ったことがある人なら、@Stateや@Binding、@ObservedObjectなどを目にしたことがあると思います。これらは全てプロパティラッパーとして定義されています。

Property Wrapperの定義法

プロパティラッパーを定義するにはクラス名や構造名、列挙名の前に @propertyWrapper とつけます。

記述が必須のプロパティは wrappedValue です。

Swift
@properyWrapper
struct Hoge {
	var value: Int

	var wrappedValue: Int {
		get { value }
		set { value = newValue }
	}

	init(wrappedValue: Int) {
		self.wrappedValue = wrappedValue
	}
}

プロパティラッパーを適応したい場合はプロパティの前に @プロパティラッパー名 を指定します。

Swift
struct Sample {
	@Hoge var value: Int
}

Property Wrapperの使用例

使用例①

まだあまりイメージが掴めないと思うので、使用例を紹介します。

与えられた文字列を全て大文字に変換する、というプロパティラッパーUpperCaseを考えてみましょう。

まずはプロパティラッパーを作成します。

UpperCase.swift
@propertyWrapper
struct UpperCase {
    private var value: String = ""

    // これが実際に入力、出力される値
    var wrappedValue: String {
        get { return value }
        set { value = newValue.uppercased() } // 渡された値を大文字に変換する
    }

    init(wrappedValue: String) {
        self.wrappedValue = wrappedValue
    }
}

次にこれを使う例を考えます。

Example.swift
struct Example {
    @UpperCase var text: String // UpperCaseを適応
}

let example = Example(text: "hello world!") // ここでwrappedValueに値が渡される
print(example.text) // HELLO WORLD!

プロパティラッパーを使うことで再利用性が増します。大文字に変換したい場合は全てUpperCaseを適応させればいいので、コードの冗長性も減ります。

使用例②

次に与えられた値がnilだった場合に、ランダムな数字を出力するプロパティラッパーDefaultValueIfNilを考えてみましょう。(あまり実用性は考えないでください)

DefaultValueIfNil
@propertyWrapper
struct DefaultValueIfNil {
    private var value: Int = 0
    
    var wrappedValue: Int? {
        get { value }
        set {
            if let newValue {
                value = newValue
            } else {
                value = Int.random(in: 0...100) // nilならランダムな数値を出力
            }
        }
    }
    
    init(wrappedValue: Int?) {
        self.wrappedValue = wrappedValue
    }
}

このプロパティラッパーを使ってみます。

Example.swift
struct Example {
    @DefaultValueIfNil var value: Int?
}

var example = Example(value: 42)
print(example.value) // Optional(42)

example.value = nil
print(example.value) // Optional(29) 👈ランダムな数値

nilが入るとランダムな数値が出力されることが確認できました。

これでプロパティラッパーの大体の動きが理解できたと思います。ここからはもう少し深いところに入っていきます。

Property Wrapperのメリット

ざっとメリットを一覧にしてみます。

  • 再利用性の向上
    • 同じような動作をするプロパティをまとめることで再利用しやすくなります
  • 可読性の向上
    • どのような動作をするプロパティかがわかりやすくなります
  • 一貫性の確保
    • 類似の振る舞いを持つプロパティに同じプロパティラッパーを適用することで、一貫性を確保できます
  • 拡張性の向上
    • 振る舞いの変更や、機能の追加がしやすくなります。

【応用編】ストレージプロパティ

プロパティラッパーを適応したプロパティの前に「_」をつけることで、プロパティラッパーの実装領域にアクセスすることができます。

これを「ストレージプロパティ」と言います。

例えば @SampleProperty というプロパティラッパーを適応した変数があるとします。

Swift
@SampleProperty var value: Int

valueの前に「_」をつけることで実装領域にアクセスできます。

Swift
value // Int型
_value // SampleProperty型

いつ使うの?

例えばSwiftUIで@Stateを適応した変数の初期化を動的に行いたい場合を考えます。

普通に値を代入しようとするとエラーが発生します。

ContentView.swift
struct ContentView: View {
	@State var value: Int

	init() {
		value = Int.random(0...100) // コンパイルエラー
	}

	var body: some View {...}
}

そこでストレージプロパティを行い、プロパティラッパー自身を初期化することで正常に動作するようになります。

Swift
init() {
	_value = State(initialValue: Int.random(in: 0...100))
}

射影値(projected value)

実装したい目的によっては外部へ何らかのプロパティを公開したいことがあります。

プロパティラッパー内に projectedValue という計算型プロパティを定義することで実現できます

これを「射影値」と言います。射影値へはプロパティの先頭に「$」をつけることでアクセスできます。

使用例①

まだイメージがわかないと思うので使用例を挙げます。

プロパティの変更回数を記録するプロパティラッパーChangeCounterを考えます。

その変更回数はprojectedValue でアクセスできるようにします。

ChangeCounter.swift
@propertyWrapper
struct ChangeCounter<Value> {
    private var value: Value
    private var changeCount = 0
    
    var wrappedValue: Value {
        get { return value }
        set { 
            value = newValue
            changeCount += 1
        }
    }
    
    var projectedValue: Int { // 変更回数を返す
        return changeCount
    }
    
    init(wrappedValue: Value) {
        self.value = wrappedValue
    }
}

このプロパティラッパーを使ってみます。

Example.swift
struct Example {
    @ChangeCounter var number: Int
}

var example = Example(number: 10)
print(example.$number) // 0

example2.number = 50
print(example.$number) // 1

example2.number = 100
print(example.$number) // 2

使用例②

SwiftUIの@Stateは射影値としてBinding型を公開しています。

@Stateを適応したプロパティに「$」をつけることでBinding型として渡すことができます。

ContentView.swift
struct ContentView: View {
    @State var text: String
    var body: some View {
        TextField("", text: $text) // Binding<String>型を渡す
    }
}

まとめ

今回はProperty Wrapperについて解説しました。

普段何気なく使っている@Stateなども、裏側ではこんなコードが動作しているんですね。コードの解像度がグッと上がったので良かったです。

ぜひご自身の手で色々と試してみてください。

ここまでのご閲覧ありがとうございました!

参考にした記事

【Swift】Property Wrapper の基礎から実装まで
この記事ではSwift5.1で導入された Property Wrapper について解説していきます。 Property Wrapper を使用すると、プロパティの「振る舞いを変更・追加し、簡単に再利用」できるようになります この記事では「
SwiftUI の @State は Property Wrapper としてどのように表現されているのか

コメント

タイトルとURLをコピーしました