本記事に関して
@EnvironmentObject、@ObservedObject、@StateObjectの違いに関して調査したことをまとめております。
環境
・Swift 5.7.2
・Xcode 14.2
・iOS 16.2
それぞれ使うにはどうすればいいのか?
Obsevable継承したクラスを生成する
class Test:ObservableObject{
@Published var isOn = false
}
@EnvironmentObject
Appでインスタンス化
struct SwiftUIPlayground5App: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(Test())
}
}
}
View内で使う
@EnvironmentObject var test:Test
@ObservedObject
View内でインスタンス化して使う
@ObservedObject var test = Test()
@StateObject
View内でインスタンス化して使う
@StateObject var test = Test()
それぞれの違いは?
@EnvironmentObject
複数Viewで共通の一つのインスタンスとして扱うことができる。
インスタンス化されるのはAppのタイミング
つまりどのViewからもアクセスが可能

@ObservedObject
作成されるのはViewでインスタンス化されるとき、子Viewに渡すことが可能

Viewのライフサイクルに合わせてインスタンスが管理される。Viewの再生成のタイミングでデータが消えるといった副作用がある。
Viewの生成でインスタンス生成、Viewの破棄でインスタンスも破棄
@StateObject
作成されるのはViewでインスタンス化されるとき、子Viewに渡すことが可能

ObservedObjectに似ているが挙動が違う。
Viewが破棄されてもインスタンスの再生成は発生せず、値は保持される。
使い所は?
@EnvironmentObject
プロジェクト全体で同じインスタンスを使いたい場合にわざわざ渡す必要がなく、シングルトンとして扱うことが可能
@StateObjectと@ObservedObject
View固有のインスタンスとして使用したい場合に使う
@StateObjectと@ObservedObjectの違いは?
結論から言うと、親Viewの再描画が行われると子Viewも再描画される。
このとき@StateObjectは破棄されないが、@ObservedObjectは破棄・再生成される
以下のような形で検証を行った(概念図)
isOnインスタンスにより子Viewから親Viewの再描画を可能とする検証

コード
inal class Test:ObservableObject{
@Published var isOn = false
}
final class Counter: ObservableObject {
@Published var count = 0
}
struct FirstView: View {
@ObservedObject var test = Test()
@State private var counter = 0
var body: some View {
NavigationView {
ZStack{
Color(test.isOn ? .blue:.green)
VStack {
Toggle("isOn", isOn: $test.isOn)
NavigationLink {
SecondView(test: test)
} label: {
Text("toSecondView")
}
}
}.navigationTitle("FirstView")
}
}
}
struct SecondView:View{
@ObservedObject var test:Test
@ObservedObject var CountObserved = Counter()
@StateObject var CountState = Counter()
@State var count = 0
var body:some View{
ZStack{
Color(test.isOn ? .blue:.green)
VStack {
Toggle("isOn", isOn: $test.isOn)
Button {
CountObserved.count += 1
} label: {
Text("ObservedCount:\(CountObserved.count)")
}
Button {
CountState.count += 1
} label: {
Text("StateCount:\(CountState.count)")
}
}
}.navigationTitle("SecondView")
.padding()
}
}
上記コードの結果(ループGif)
isOnを変更し親Viewの変更をした時に、StateCountはそのままだがObservedCountはクリアされることがわかる。

まとめ
・@EnvironmentObject、@ObservedObject、@StateObjectは振る舞いが違うため用途によって使い方を決める必要がある。
・特に@ObservedObjectと@StateObjectに要注意で親Viewの再描画で初期化したいか否かで使いわけること。
SwiftUIのおすすめ参考書籍
図とサンプルコードが多くわかりやすく、手を動かしながら学べるのはもちろんですが、状態やデータフローに関する解説や、swiftuiから uikitを使う方法、uikitから swiftuiを使う方法などフレームワークの関連性まで説明していてとても参考になります。
コメント