【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はProperty Wrapper(プロパティラッパ)の1つ
- クラスのプロパティの更新がリアルタイムにビューに反映する役割
- 監視するプロパティには@Publishedを付与
- クラスはObservableObjectプロトコル準拠
- インスタンス化した変数に@ObservedObjectまたは@EnvironmentObjectを付与
では実際に@EnvironmentObject
を使用する方法をみていきます。
@EnvironmentObjectの使い方
クラスで定義しているプロパティの値が変化する際に複数のビュー(表示)も同時に更新したい場合に@EnvironmentObject
を使います。使う流れは以下の通りです。
- クラスをObservableObjectプロトコルに準拠して定義
- 監視したいプロパティに@Publishedを付与
- 親ビューでインスタンス化した変数には@ObservedObjectを付与
- 子ビュー呼出時にインスタンス変数を指定
- 子ビューでインスタンス化した変数に@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()
}
}
@EnvironmentObjectと@ObservedObjectの違い
クラスのプロパティの変更を監視するために使用する@ObservedObject
/@EnvironmentObject
ですが両者の違いは変更監視できる範囲と再描画されるビューが異なります。
両者の違い
- @EnvironmentObjectは複数のビューにまたがる変更を監視して再描画
- @ObservedObjectは1つのビュー内の変更を監視して再描画
違いを確かめてみる
先ほどのファイル構造を整理してみます。親ビューから複数の子ビューを呼び出し、全てのビューで表示している値はクラスのプロパティに基づいた値です。全てのファイルでそれぞれクラスをインスタンス化して表示させます。
├── 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
は階層が深くなればなるほとややこしく複雑になってしまいます。しかしシングルトンなら必要な階層のみに定義するだけで良くなります。
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。