IT アプリ開発

【SwiftUI】PropertyWrapperの作成

2022年4月2日

Swiftuiにはプロパティラッパーという機能があります。
プロパティラッパーという名の通り、プロパティに対してラップして、そのラップで処理を定義します。
外からラップされたプロパティにアクセスするには、ラップを通過しないといけません。
そのラップに処理を定義するのがプロパティラッパーです。
そしてその処理を定義するのが、wrappedValueというプロパティです。
詳細は以下で話していこうと思います。

 

すでに定義されているさまざまな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の順に処理が行われました。
想定通りで、プロパティの値を更新/代入した後、裏で指定した値を加えて返されるような処理になってます。

 

-IT, アプリ開発
-,

© 2022 Yosshi Blog Powered by AFFINGER5