SwiftUI Tutorialsに登場したProperty Wrappersまとめ


はじめに

自分がApple公式のSwiftUI Tutorialsを通して学んだ時に登場したProperty Wrappersがそれぞれどういう役割かとかを忘れちゃいそうなので備忘のために書き残すことにしました。

watchOSやmacOSのAppを作る予定は現状なかったので、Chapter3までに登場したProperty Wrappersの紹介になります。
これから同じくSwiftUIを始めて勉強する方々の参考になればと思います。

Property Wrappersとは

Swift5.1で実装された機能です。
プロパティのget/setに関わる制御を共通化するような仕組みです。

この後紹介する@StateEnvironmentObjectはProperty Wrappersですし、自分で新たなものを定義することが可能です。

SwiftUI Tutorialsに登場したProperty Wrappers

@￰State

State | Apple Developer Documentation

通常SwiftUIのViewはstructで定義していくためプロパティの更新ができないが、@Stateをつけて宣言することでそのプロパティの値の読み書きができる。

@Stateで宣言したプロパティはViewのbodyもしくはViewから呼び出されるメソッドからのみアクセスする必要があるとAppleから推奨されているので、基本的にはprivateをつけて宣言するのが一般的のようだ。

Viewにプロパティの値を渡したい時は$を変数名につけることで実現できる。

struct ToggleView: View {
    /// トグルのスイッチの状態(初期値: false)
    @State private var isOn = false

    var body: some View {
        // isOnの値を監視($をつける!)
        Toggle(isOn: $isOn) {
            Text("スイッチを切り替える")
        }
    }
}

これで下記画像のようにスイッチの状態を切り替えることができるようになる。

@￰Publishedと@￰EnvironmentObject

Published | Apple Developer Documentation
EnvironmentObject | Apple Developer Documentation

チュートリアルでは、ObservableObjectを準拠しているクラスのプロパティに@Publishedを付けることで監視側がデータの変更を取得できるようにしている。
@EnvironmentObjectをつけてプロパティを宣言することで、複数のViewに共通のインスタンスを渡して値を監視させられます。

下記コードでは親のContentView内で.environmentObject(User())でインスタンスを渡して子ビューであるTextViewButtonsViewで値を参照しています。

final class User: ObservableObject {
    @Published var name = "Taro"
    @Published var age = 18
}

struct ContentView: View {

    var body: some View {
        VStack {
            TextView()
            ButtonsView()
        }
        .environmentObject(User())
    }
}

struct TextView: View {
    @EnvironmentObject var user: User

    var body: some View {
        Text("I'm \(user.name).")
        Text("I'm \(user.age) years old.")
    }
}

struct ButtonsView: View {

    @EnvironmentObject var user: User

    var body: some View {
        HStack {
            Button(action: {
                self.user.age += 1
            }) {
                Text("歳をとる")
            }
            Button(action: {
                self.user.age -= 1
            }) {
                Text("若返る")
            }
        }
    }
}



実行したアプリでは、ボタンを押したときのユーザの年齢が動的に変更される。

これで一応動くのですが、environmentObjectはアプリ全体で共通使用するデータのやりとりをするので、正しくはContentViewから.environmentObject(User())を削除し、代わりにXXXApp.swift内に下記のようにして記述するのが適切なようです。

そうでないと、print文で確認すると更新はされているがアプリの見た目は変わらないというような、正常に動作しないことがありました。

XXXApp.swift
struct SampleSwiftUIApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView().environmentObject(User()) //ここで共通する
        }
    }
}

@￰StateObject

StateObject | Apple Developer Documentation

一度初期化すると、フレームワーク側で@stateObjectがついたプロパティの値が保持され続けるのでビューの再描画が発生しても値が初期化されなくなります。
@ObservedObjectを使用していると初期化されてしまうので、再描画されても変わってほしくない場合は@stateObjectを使うと良さそうです。

チュートリアルを見る感じだと、こちらも初期化してそのままプロパティを保持して欲しいことを考えると起動直後に宣言して.environmentObjectで渡すのが適しているという感じでしょうか。

XXXApp.swift
struct SampleSwiftUIApp: App {

    @StateObject var hogeData = HogeData() // 任意の保持され続けて欲しいデータ

    var body: some Scene {
        WindowGroup {
            ContentView().environmentObject(hogeData) // そのデータを渡す
        }
    }
}

@￰Binding

Binding | Apple Developer Documentation

データを格納するプロパティと、そのデータを変更/更新するビューを双方向に接続します。
ということは、@Bindingで設定したプロパティが更新されれば、それと接続している他のビューも更新されるということになるようです。

final class ToggleState: ObservableObject {
    @Published var isOn = false
}

struct ContentView: View {
    @EnvironmentObject var toggle: ToggleState

    var body: some View {
        VStack {
            ToggleAView(isOn: $user.isOn)
            ToggleBView(isOn: $user.isOn)
        }
    }
}

struct ToggleAView:View {
    @Binding var isOn: Bool

    var body: some View {
        Toggle("スイッチA", isOn: $isOn)
    }
}

struct ToggleBView:View {
    @Binding var isOn: Bool

    var body: some View {
        Toggle("スイッチB", isOn: $isOn)
    }
}

上記のコードは同じプロパティをBindingしているので、片方のトグルを切り替えると、その状態がもう片方の状態にも同期されて切り替わるようになります。

@￰Environment

Environment | Apple Developer Documentation

EnvironmentValuesに定義されているビューの環境の設定値を取得/更新したいときに使います。
下記コードは取得の例です。

struct ContentView: View {
    @Environment(\.timeZone) var timeZone
    @Environment(\.calendar) var calendar
    @Environment(\.locale) var locale

    var body: some View {
        VStack {
            Button(action: {
                print(timeZone) // Asia/Tokyo (current)
                print(calendar) // gregorian (current)
                print(locale)   //en (current)
            }) {
                Text("button")
            }
        }
    }
}

EnvironmentValuesは上記のようにtimeZoneやcalendar等の値意外にも豊富に定義されています。
また、自分でEnvironmentValuesの値を新たに定義することもできるそうです。

おわりに

ひとまず、SwiftUI Tutorialsに登場するProperty Wrappersをそれぞれ調べてみて、それぞれがどういうものなのかはある程度把握できました。
他の記事を見る感じだと、今回紹介したものがやはりよく紹介されているのを見かけるので頻繁に活用していくのだと思われます。

これから実際にSwiftUIを使ったコーディングをしていくと思うので、これらを駆使しながら実装していきます。

Property Wrappersを複数種類組み合わせたときの挙動とかのケーススタディができていないのと、@ObservedObject@AppStorage等の他のProperty Wrappersのまとめができていなかったりするので、こういうときどれを使うのが良いかがまだ不明な状態ですが、また色々調べながら進めて行けたらと思います。

参照 

  1. 【Swift 5.1】Property Wrappersとは? | 2速で歩くヒト
  2. SwiftUIの機能 @State, @ObservedObject, @EnvironmentObjectの違いとは
  3. SwiftUIのProperty Wrappersとデータへのアクセス方法
  4. EnvironmentValuesを制するものはSwiftUIを制する

※チュートリアルと各Property Wrappersのリンクは既に上記内で共有しているので省略させていただいています。