Swiftuiにはプロパティラッパーという機能があります。
プロパティラッパーという名の通り、プロパティに対してラップして、そのラップで処理を定義します。
外からラップされたプロパティにアクセスするには、ラップを通過しないといけません。
そのラップに処理を定義するのがプロパティラッパーです。
そしてその処理を定義するのが、wrappedValueというプロパティです。
詳細は以下で話していこうと思います。
Contents
すでに定義されているさまざまなPropertyWrapperたち
Swiftuiではプロパティラッパー機能を用いて、すでに定義されているものがあったリします。
プロパティラッパーでは以下のようなものがあります。
定義ずみPropertyWrapper | 説明 |
---|---|
@State | 1つの変数に対してバインディングを行う |
@Binding | Stateとよく対比されるが、@Stateを宣言したView以外でそのState変数を利用したい場合に、参照型(State変数が格納されているメモリ上のアドレスへ参照)として@Bindingという型がある。 |
@observedObject | 1つのインスタンスに対してバインディングを行う |
@EnvironmentObject | その名の通り環境なので、App全体でインスタンスなどを共有できるPropertyWrapper |
@AppStorage | UserDefault機能を直接使わず、@AppStorageを用いることで簡単にUserDefault機能を使用する。 |
よく見るものばかりですね。
これらは@Stateであれば「そのプロパティの値が更新されたら、Viewを再構築する。」などプロパティラッパーを宣言することで、簡単に機能を付与することができます。
であれば、プロパティの値が更新された際に何か処理を追加したいなーと思うこともあると思います。
そこでここでは自分でプロパティラッパーを作成することについて説明していこうと思います。
自作のPropertyWrapperを作成する
以下のプロパティラッパーは、指定した値を加算した結果を返すようなプロパティラッパーです。
@propertyWrapperを定義した場合、必ずwrappedValueというプロパティを指定する必要があります。
これは、get:値を取得する場合の処理と、set:値を更新する場合の処理を定義するプロパティになります。
以下では、
getはそのままreturn、setは今回の処理である値の加算の処理を行うので、add_valueという数値分加算して値の更新を行うような処理を加えています。
@propertyWrapper struct AddIntValue { var add_value:Int // 加える値 private var value: Int // 受け取る値 var wrappedValue:Int{ // 必ず、propertyWrapperを指定した場合は設定必要(wrappedValueはgetとsetを定義するプロパティ) get { value } set { value = newValue + add_value // 指定した値を入れて返します } } // var projectedValue:int{ // これは任意で指定できるprojectedValue。この値にアクセスするには$をつけて$変数名でアクセスします。 // } init(wrappedValue :Int, add_value:Int){ self.value = wrappedValue self.add_value = add_value } } struct TestPropertWrapper : View { @AddIntValue(add_value : 20) public var val:Int = 20 var body: some View{ VStack(){ Button(action: { self.val = 123 }, label: { Text("\(self.val)") }) } } }
これでは、Buttonのaction内の、self.val=123で以下のようなエラーになります。
Cannot assign to property: 'self' is immutable
上でも話した通り、構造体struct内で宣言したプロパティに値を代入/更新することはできません。
自分で定義したプロパティラッパーにはdynamicPropertyプロトコルが入っていないため更新などが走りません。
そのため上記のようなimmutableのエラーになります。
代入などする場合は、mutatingなどの宣言が必要になったりします。
@Stateをプラスアルファで宣言しても問題ないですので、ここで宣言してみます。
PropertyWrapperをまとめる
上記のエラーを無くすため、@Stateも宣言します。そうするとエラーは無くなります。
作成したプロパティラッパーAddIntValueには、dynamicプロトコルが入ってません。
構造体は値更新できないため、
以下のように@Stateなどをつけることで、値更新ができるようになります。
@propertyWrapper struct AddIntValue { var add_value:Int // 加える値 private var value: Int // 受け取る値 var wrappedValue:Int{ get { return value } set { value = newValue + add_value } } // var projectedValue:int{ // これは任意で指定できるprojectedValue。この値にアクセスするには$をつけて$変数名でアクセスします。 // } init(wrappedValue :Int, add_value:Int){ self.value = wrappedValue self.add_value = add_value } }
そして、上記で定義したプロパティラッパーを通常のViewで使用してみたいと思います。
上のプロパティラッパーの処理を適用したい、プロパティに宣言します。
下で、valというプロパティにAddIntValueプロパティラッパーを宣言して、適用します。
こうすることで、valの値が更新されたりした場合、AddIntValueの処理がバックで行われます。
そして宣言についてです。
@AddIntValue(add_value : 20)とあります。
これはAddIntValueプロパティラッパーのインスタンスを生成しています。
定義したAddIntValueプロパティラッパーのinitを見ると、引数が2つですが、第2引数のadd_valueしか入れていません。
第1引数のwrappedValueはどうなったのか。
この値は、自動的に対象となっているプロパティに値が入ると、その値をwrappedValueが拾って格納します。
なので、今回の場合だと、
valに20が入ると、@AddIntValue(wrappedValue: 20, add_value: 20)
valに50が入ると、@AddIntValue(wrappedValue: 50, add_value: 20)
というようになるイメージです。
そのため、宣言するときは、wrappedValueを指定した箇所は指定しなくて良いです。
struct TestPropertWrapper : View { @State @AddIntValue(add_value : 20) public var val:Int = 20 var body: some View{ VStack(){ Button(action: { self.val = 123 }, label: { Text("\(self.val)") }) } } }
今回は、上記のような指定した値を加えるという処理のプロパティラッパーを作成しました。
@<プロパティラッパー名>を宣言することで、プロパティに処理を付加させることができました。
もちろん今回のように値を加えるというものだけでなく、値が更新されたら、firestoreやUserDefaultなどにデータを格納するなどの処理を書くこともできます。
1つプロパティラッパーを作成/定義しておくことで、簡単に上のように宣言することで、その機能を追加することができてしまいます。
PropertyWrapperの処理順番
getter、setter、buttonクリックした際に、それぞれでログを見てみたいと思います。
@propertyWrapper struct AddIntValue { var add_value:Int // 加える値 private var value: Int // 受け取る値 var wrappedValue:Int{ get { var _ = print("getter処理開始時:\(self.value)") return value } set { var _ = print("setter処理開始時:\(self.value)") value = newValue + add_value } } // var projectedValue:int{ // これは任意で指定できるprojectedValue。この値にアクセスするには$をつけて$変数名でアクセスします。 // } init(wrappedValue :Int, add_value:Int){ self.value = wrappedValue self.add_value = add_value } } struct TestPropertWrapper : View { @State @AddIntValue(add_value : 20) public var val:Int = 20 var body: some View{ VStack(){ Button(action: { var _ = print("buttonクリック処理:\(self.val)") self.val = 123 }, label: { Text("\(self.val)") }) } } }
そうすると、
ボタンクリック → setter → getterの順に処理が行われました。
想定通りで、プロパティの値を更新/代入した後、裏で指定した値を加えて返されるような処理になってます。