この記事では 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
によってラップされた値の投影された値として使用することができます。
先程の DoSomething
に projectedValue
の機能を追加すると次のようになります。
@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
イニシャライザ
DoSomething
の value
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()
}
}
結果としてView
の body
が呼ばれる仕組みが分からなかったので、実際にUIを動かしてみることはできませんでしたが、body
が呼ばれる仕組みについてはこちらの記事に詳しく考察と解説をされていました。
Field Descriptor
を使ったリフレクションで、View
の State
プロパティに 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