SwiftにはProperty Observerと呼ばれる、変数に新たな値がセットされるタイミングで関数の呼び出しを定義できる仕組みが存在します。例えば、didSetやwillSetなどです(ここでは詳しく触れないので、こちらを参照ください)。
そしてこの仕組みをさらに発展させ、ある変数に対する変更や取得に対してさらに簡潔に、複雑な処理を定義できるようにした仕組みが今回紹介するProperty Wrapperです。
Property Wrapperは、ある変数に対する変化に対応して非常に多くの処理を一斉に行えるというその特徴から、SwiftUIでも頻繁に使われる機能です。
ここではその具体的な実装方法、使い方について説明していきます。
Property Wrapperの基本、その実装
下記のようにwrappedValue(変数の値が入ります)を定義したstructに、@propertyWrapperのattributeをつけるとProperty Wrapperが完成します。
Property Wrapperは下記のように、@FillZeroのような形で@{struct名}をvarの左側につけることで使用することができます。
@propertyWrapper
struct FillZero {
private var value: String
private static func fill(_ input: String) -> String {
guard let intVal = Int(input) else { return input }
if 0 < intVal && 10 > intVal {
return "0" + input
} else {
return input
}
}
init(wrappedValue: String) {
value = Self.fill(wrappedValue)
}
var wrappedValue: String {
get { value }
set { value = Self.fill(newValue) }
}
}
@FillZero var num: String = "8"
@FillZero var num2: String = "18"
print(num) // 08
print(num2) // 18
上記のように、代入したwrappedValueに対して特定の処理を行うことができます。
Projected Value
また、Property Wrapperにはもう一つ、Projected Valueと呼ばれるものを定義できます。こちらはある変数に対し、任意の処理を行った結果を返すことのできる仕組みです。
例えば、上記変数において、StringをIntに変換した値も欲しいものとします。
@propertyWrapper
struct FillZero {
...
// 追加
var projectedValue: Int? {
return Int(value)
}
}
@FillZero var num: String = "8"
print($num! * 2) // 16
$マーク + 変数名でprojected valueに自動で変換され、ご覧の通り型が違っていても任意の処理を施された値が返されます。
Property Wrapperの応用
パラメータの追加
ゼロ埋めの桁数を調節したい場合を考えます。
基本的にはイニシャライザに変数を追加し、Property Wrapperの引数を取ることで他のパラメータを変換処理の中に追加できます。
@propertyWrapper
struct FillZero {
private var value: String
// 追加
let digits: Int
private static func fill(_ input: String,
digits: Int) -> String {
// 関数の中身変更
guard input.count <= digits else {
return input
}
let zerosNeeded = digits - input.count
let zeroFill = String(repeating: "0", count: zerosNeeded)
return zeroFill + input
}
init(wrappedValue: String,
digits: Int) {
value = Self.fill(wrappedValue,
digits: digits)
self.digits = digits
}
var wrappedValue: String {
get { value }
set { value = Self.fill(newValue,
digits: digits) }
}
var projectedValue: Int? {
return Int(value)
}
}
@FillZero(digits: 2) var num: String = "8" // 08
@FillZero(digits: 3) var num2: String = "18" // 018
Copy on Writeの実装
参照型の値を書き換える際に、参照先そのものを入れ替える、Copy on Writeも以下のように実装することでスッキリと書くことができます。
struct WrappedValueType {
var intValue = 0
private var reference = ReferenceTypeClass(intValue: 0)
@ReferenceTypeWrapper var referenceIntValue: Int = 0
init(intValue: Int = 0) {
self.intValue = intValue
}
}
@propertyWrapper
struct ReferenceTypeWrapper {
private var reference: ReferenceTypeClass
init(wrappedValue: Int) {
self.reference = ReferenceTypeClass(intValue: wrappedValue)
}
var wrappedValue: Int {
get {
reference.intValue
} set {
if isKnownUniquelyReferenced(&reference) {
reference.intValue = newValue
} else {
reference = ReferenceTypeClass(intValue: newValue)
}
}
}
}
let value1 = WrappedValueType(intValue: 0)
var value2 = value1
value2.referenceIntValue = 1
print(value1.referenceIntValue) // 0
value2を書き換えても、value1とは別のインスタンスが生成されているため、value1では変化しません。
まとめ
- Property WrapperはProperty Observerを一気に、そして簡潔に定義できるようにしたもの。
- StructにwrappedValueとそのイニシャライザを定義し、
@propertyWrapperアトリビュートをつけることでPropertyWrapperが定義できる。 - PropertyWrapper中の
projectedValue部分で、${変数名}の形で任意の型の処理済みの値を返すことができるようになる。 - パラメータを付与したり、参照型の値のValue Semanticsを満たしたい場合にも、実装を工夫することでProperty Wrapperを使うことができる。
最後に
こちらは私が書籍で学んだ学習内容をアウトプットしたものです。
わかりにくい点、間違っている点等ございましたら是非ご指摘お願いいたします。