【Swift UI】@EnvironmentObjectの意味と使い方!クラスを別ビューで共有する

この記事からわかること

  • Swift@EnvironmentObjectとは?
  • 意味使い方メリット
  • ObservableObjectプロトコルクラスでの定義方法
  • @Publishedの使い方
  • 別ビュー間でクラスを共有する方法
  • シングルトンでクラスを共有する方法

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

Swift UIで別ビュー間でクラスのプロパティの更新を監視し共有するために使用する「@EnvironmentObject」やそれに伴う「@Published」などの使い方やメリットをまとめていきたいと思います。

@EnvironmentObjectとは?

@EnvironmentObject@State@Bindingなどと同じくSwift5.1から導入された機能「Property Wrapper(プロパティラッパ)」の1つです。プロパティラッパの特徴はプロパティ(変数)に対する操作や処理をカプセル化して定義することで特定のキーワードを追加するだけで任意の動作や挙動を行わせることができるデータ構造です。

@EnvironmentObject@ObservedObjectと同じく@Publishedと組み合わせて使用することで、クラスのプロパティの変更を監視し、更新された時にビューに表示されている値も更新するためのプロパティラッパです。つまりクラスのプロパティの更新がリアルタイムにビューに反映されるようになります。

@Publishedもプロパティラッパの1種でcombineフレームワークから提供されている機能の1つです。@Publishedが付与されたプロパティは監視状態になります。これで値が変更された時にその変化を感知することができるようになります。

@Publishedが付与されているプロパティが更新され、ビューが再描画されるためにはそのクラスがObservedObjectプロトコルに準拠しインスタンス化した変数に@ObservedObjectまたは@EnvironmentObjectが指定されている必要があります。

ポイント

では実際に@EnvironmentObjectを使用する方法をみていきます。

@EnvironmentObjectの使い方

クラスで定義しているプロパティの値が変化する際に複数のビュー(表示)も同時に更新したい場合に@EnvironmentObjectを使います。使う流れは以下の通りです。

  1. クラスをObservableObjectプロトコルに準拠して定義
  2. 監視したいプロパティに@Publishedを付与
  3. 親ビューでインスタンス化した変数には@ObservedObjectを付与
  4. 子ビュー呼出時にインスタンス変数を指定
  5. 子ビューでインスタンス化した変数に@EnvironmentObjectを付与

クラスはObservableObjectプロトコルに準拠して定義

@EnvironmentObjectを使用して観測するクラスはObservableObjectプロトコルに準拠している必要があります。クラス名:ObservableObject形式でプロトコルを指定してあげましょう。


class Greeting:ObservableObject{
    var greet = "Hello World"

    func ChangeGreet(){
      greet = "こんにちは世界"
    }
}

監視したいプロパティに@Publishedを付与

変更を監視したいプロパティに@Publishedを付与しておきます。@Publishedをつけ忘れると更新しても変更されないので注意してください。


class Greeting:ObservableObject{
    @Published  var greet = "Hello World"

    func ChangeGreet(){
      greet = "こんにちは世界"
    }
}

プロパティはlet(定数)ではProperty wrapper can only be applied to a 'var'のようなエラーになるので変化を許容できるvarで宣言しておきます。

親ビューでインスタンス化した変数には@ObservedObjectを付与

続いて親ビューでObservedObjectプロトコルに準拠させたクラスをインスタンス化します。その際は@EnvironmentObjectではなく@ObservedObjectを付与します。


struct ContentView: View {
    // 親には@ObservedObjectを指定
    @ObservedObject  var greeting = Greeting()
    
    var body: some View {
        VStack{
            VStack{
                Text("\(greeting.greet)").padding()
                HStack {
                    Text("ContentView:")
                    Button("Change!!"){
                        greeting.ChangeGreet()
                    }
                }
            }.padding()
            // 子ビュー呼び出し
            ChangePropertyView().environmentObject(greeting)
            // 子ビュー呼び出し
            DisplayPropertyView().environmentObject(greeting)
        }           
    }
}

子ビュー呼出時にインスタンス変数を指定

親ビューから子ビューを呼び出す際にはenvironmentObjectを使って観測したいインスタンスを渡す必要があります。

// 子ビュー呼び出し environmentObject(インスタンス)を渡す
ChangePropertyView().environmentObject(greeting)
// 子ビュー呼び出し  environmentObject(インスタンス)を渡す
DisplayPropertyView().environmentObject(greeting)

子ビューでインスタンス化した変数に@EnvironmentObjectを付与

最後に子ビュー側で@EnvironmentObjectを付与してインスタンス化し変数に格納します。使用する全てのファイルでインスタンス化する必要があるので注意してください。これでいずれかのファイルからクラスのプロパティの値が変化した時に複数のビューも連動して更新されるようになります。


struct ChangePropertyView: View {
    
    @EnvironmentObject  var greeting:Greeting
    
    var body: some View {
        HStack{
            Text("ChangePropertyView:")
            Button("Change!!"){
                greeting.ChangeGreet()
            }
        }
    }
}

struct DisplayPropertyView: View {
    
    @EnvironmentObject  var greeting:Greeting
    
    var body: some View {
            Text("\(greeting.greet)").padding()
    }
}
Swiftの@EnvironmentObjectを使用してクラスのプロパティを観測しビューを再描画している様子

@EnvironmentObjectと@ObservedObjectの違い

クラスのプロパティの変更を監視するために使用する@ObservedObject/@EnvironmentObjectですが両者の違いは変更監視できる範囲と再描画されるビューが異なります。

両者の違い

違いを確かめてみる

先ほどのファイル構造を整理してみます。親ビューから複数の子ビューを呼び出し、全てのビューで表示している値はクラスのプロパティに基づいた値です。全てのファイルでそれぞれクラスをインスタンス化して表示させます。

├── Swiftプロジェクト
│   ├── プロジェクト名
│        ├──  ContentView.swift(親)  // プロパティの値を変更する処理かつ表示
│        ├──  ChangePropertyView.swift(子) // プロパティの値を変更する処理だけ
│        └──  DisplayPropertyView.swift(子)  // プロパティの値を表示しているだけ

両者それぞれのインスタンス化の書式が異なる点にも注意してください。

子ビューが@ObservedObjectの場合

@ObservedObject  var greeting = Greeting()

@ObservedObjectの場合は変更があったファイルのみ更新され再描画されます。例えば「ContentView.swift」で変更した場合、「ContentView.swift」のビューは再描画されますが、他のファイル(ビュー)は未更新のままです。

クラスは参照型のデータ構造なので扱っているデータは共通ですが@ObservedObjectの場合は実際のデータは変化しても「変化したことを気づけない」のでビューは再描画されません。

子ビューが@EnvironmentObjectの場合

@EnvironmentObject  var greeting:Greeting

@EnvironmentObjectの場合はいずれかのファイルから変更を観測すると全てのファイルのクラスのプロパティが一斉に更新されビューが再描画されます。

このようにインスタンス化時に指定するプロパティラッパの違いだけ(厳密には書式が少し異なりますが)でクラスのプロパティの変化を観測でき更新できる範囲を変更できます。

Fatal error: No ObservableObject of type ~ found. A View.environmentObject(_:) for ~ may be missing as an ancestor of this view.

Fatal error: No ObservableObject of type ~ found. A View.environmentObject(_:) for ~ may be missing as an ancestor of this view.

このエラーは.environmentObject(インスタンス変数)を付与し忘れている場合に発生しました

@EnvironmentObjectを使用せずに別ビューで変更を共有する

@EnvironmentObjectを使用せずに別ビューで変更を共有する方法としてシングルトンな設計にすることでも実装可能です。@ObservedObjectを使用するのは変わらず、Greetingクラスに自身のインスタンスを持つsharedプロパティを定義します。

おすすめ記事:Singletonパターンとは?

class Greeting:ObservableObject{
    
    static var shared:Greeting = Greeting()
    
    @Published  var greet = "Hello World"
    
    func ChangeGreet(){
        greet = "こんにちは世界"
    }
}

あとは以下のように全てに@ObservedObjectを付与します。これでContentViewから変更を加えてもDisplayPropertyViewにも変更が反映されます。

struct ContentView:: View {
    // 全てに@ObservedObjectを指定
    @ObservedObject  var greeting = Greeting.shared
    
    var body: some View {
        VStack{
            VStack{
                Text("ContentView:\(greeting.greet)").padding()
                HStack {
                    Text("ContentView:")
                    Button("Change!!"){
                        greeting.ChangeGreet()
                    }
                }
            }.padding()
            // 子ビュー呼び出し
            ChangePropertyView() //.environmentObject(greeting)
            // 子ビュー呼び出し
            DisplayPropertyView() //.environmentObject(greeting)
        }
    }
}

struct ChangePropertyView: View {
    
//    @EnvironmentObject    var greeting:Greeting
    // 全てに@ObservedObjectを指定
    @ObservedObject  var greeting = Greeting.shared
    
    var body: some View {
        HStack{
            Text("ChangePropertyView:")
            Button("Change!!"){
                greeting.ChangeGreet()
            }
        }
    }
}

struct DisplayPropertyView: View {
    
//    @EnvironmentObject    var greeting:Greeting
    // 全てに@ObservedObjectを指定
    @ObservedObject  var greeting = Greeting.shared
    
    var body: some View {
            Text("DisplayPropertyView:\(greeting.greet)").padding()
    }
}

シングルトンにするメリット

シングルトンにする一番のメリットはenvironmentObjectを使用せずに済むことです。environmentObject階層が深くなればなるほとややこしく複雑になってしまいます。しかしシングルトンなら必要な階層のみに定義するだけで良くなります。

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index