15
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

やさしい Property Wrapper

Posted at

この記事では Swift の Property Wrapper が何かをやさしく解説します。

PropertyWrapper とは

公式ドキュメントには下記のように説明されています。

A property wrapper adds a layer of separation between code that manages how a property is stored and the code that defines a property.

プロパティラッパーは、プロパティの保存方法を管理するコードと、プロパティを定義するコードとの間に、分離の層を追加します。

上記の説明を理解するために、試しにコードを書いて確認していきましょう!

value プロパティに値を保存する際、何か処理を実行する A struct があるとします。

struct A {
    // プロパティの保存方法を管理するコードと、プロパティを定義するコード
    private var _value: Bool = false // <- 管理
    var value: Bool { // <- 定義
        set { // <- 保存方法
            print("Do something")
            _value = newValue
        }
        get {
            _value
        }
    }
}

これを propertyWrapper を使うとこのように書けます。

// 分離の層
@propertyWrapper
struct DoSomething {
    private var _value = false // <- 管理
    var wrappedValue: Bool {
        get { _value }
        set { // <- 保存方法
            print("Do something")
            _value = newValue
        }
    }
}

struct A {
    @DoSomething var value: Bool // <- 定義
}

確かに、プロパティの定義と保存方法を分離できてますね!

メリット

For example, if you have properties that provide thread-safety checks or store their underlying data in a database, you have to write that code on every property. When you use a property wrapper, you write the management code once when you define the wrapper, and then reuse that management code by applying it to multiple properties.

例えば、スレッドセーフのチェックを行うプロパティや、基礎となるデータをデータベースに保存するプロパティがある場合、すべてのプロパティにそのコードを書かなければなりません。プロパティ・ラッパーを使用すると、ラッパーの定義時に管理コードを一度だけ記述し、その管理コードを複数のプロパティに適用して再利用することができます。

property wrapper を活用すると、管理コードを再利用することができるのがメリットのようです。

projectedValue について

上記が propertyWrapper のメインとする機能なのですが、さらに追加機能として projectedValue があります。projectedValue は名前の通り propertyWrapper によってラップされた値の投影された値として使用することができます。

先程の DoSomethingprojectedValue の機能を追加すると次のようになります。

@propertyWrapper
struct DoSomething {
    private var _value = false
    var projectedValue = { // <- ここを追加
        print("Do additional something")
    }
    var wrappedValue: Bool {
        get { _value }
        set {
            print("Do something")
            _value = newValue
        }
    }
}

定義したプロパティ名の前に $ を付けて参照します。

var a = A()
print(a.$value()) // Do additional something

イニシャライザ

DoSomethingvalue property の初期値は false ですが、イニシャライザを定義することで、変数の定義時に初期値を設定することができます。

struct DoSomething {
    init(wrappedValue: Bool) {
        self._value = wrappedValue
    }
    ...
}

struct A {
    @DoSomething var value = true
}

使い方の例

さて、Property Wrapper が何か少しわかってきたところで、実際にどのように利用できるのか見ていきましょう。
ここでは、proposal に記載されている例の一部を紹介します。
Userdefaults へのアクセスをラップしたものです。

@propertyWrapper
struct UserDefault<T> {
  let key: Key
  let defaultValue: T

  var wrappedValue: T {
    get {
        return UserDefaults.standard.object(forKey: key.rawValue) as? T ?? defaultValue
    }
    set {
        UserDefaults.standard.set(newValue, forKey: key.rawValue)
    }
  }
}

extension UserDefault {
    enum Key: String {
        case isFooFeatureEnabled
    }
}

Struct A {
    @UserDefault(key: .isFooFeatureEnabled, defaultValue: false)
    static var isFooFeatureEnabled: Bool
}

このような使い方をすることで、ユーザーデフォルトへのアクセスをスッキリと書くことができて良さそうですね!
proposal には他の例も書かれていますし、他にも応用できそうなものはないか考えてみると面白そうです!

State を自前実装してみる

最後に、SwiftUI の @State を自前実装したらどうなるか考察してみました。

import SwiftUI
import Combine

class Store<T> {
    var value: T

    init(value: T) {
        self.value = value
    }
}

@propertyWrapper
struct State2<T> {

    private let store: Store<T>
    var viewGraph: ViewGraph? // <- フレームワークによって注入される

    init(wrappedValue value: T) {
        self.value = value
        self.store = Store(value: value)
    }

    private var value: T

    var projectedValue: Binding<T> {
        Binding {
            print("get \(store.value)")
            return store.value
        } set: { newValue in
            store.value = newValue
            print("set \(store.value)")

            viewGraph.render() // <- body を呼ぶ
        }
    }

    var wrappedValue: T {
        nonmutating set {
            store.value = newValue
        }
        get {
            store.value
        }
    }
}

struct PlayerView: View {
//    @State private var isPlaying: Bool = false
    @State2 private var isPlaying: Bool = false

    var body: some View {
        PlayButton(isPlaying: $isPlaying)
    }
}

struct PlayButton: View {
    @Binding var isPlaying: Bool

    var body: some View {
        Button(action: {
            self.isPlaying.toggle()
        }) {
            Image(systemName: isPlaying ? "pause.circle" : "play.circle")
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        PlayerView()
    }
}

結果としてViewbody が呼ばれる仕組みが分からなかったので、実際にUIを動かしてみることはできませんでしたが、body が呼ばれる仕組みについてはこちらの記事に詳しく考察と解説をされていました。

Field Descriptor を使ったリフレクションで、ViewState プロパティに ViewGraph というオブジェクトを注入し、値が更新されたタイミングで ViewGraph を使って更新させているようです。勉強になりますね!

今回は以上です。ではでは 👋


参考リンク
https://docs.swift.org/swift-book/LanguageGuide/Properties.html
https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md#introduction
https://kateinoigakukun.hatenablog.com/entry/2019/06/08/232142

15
11
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?