[SwiftUI]EnvironmentObjectをウィンドウごとに別々のインスタンスにしたい
UIの状態を管理するクラスをEnvironmentObjectにしておくと、子や孫Viewにインスタンスを渡したり、シングルトンを用いなくても簡単に子孫のViewやButtonからUIの更新ができて便利ですよね。例えばこんな感じに
import SwiftUI
@main
struct MultiWindowCounterApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(Counter()) // ここでインスタンスを生成し、EnvironmentObjectにセット
}
}
}
class Counter: ObservableObject {
@Published var count = 0
func increment() { count += 1 }
func reset() { count = 0 }
}
struct ContentView: View {
@EnvironmentObject var counter: Counter // インスタンスを共有できる
var body: some View {
VStack {
Text("\(counter.count)")
.padding()
ControlView()
}.padding()
}
}
struct ControlView: View {
@EnvironmentObject var counter: Counter // インスタンスを共有できる
var body: some View {
HStack {
Button("Increment") {
self.counter.increment()
}
Button("Reset") {
self.counter.reset()
}
}
}
}
MultiWindowCounterApp
内でContentView
を生成する際に、EnvironmentObjectにCounter
クラスのインスタンスを生成して渡しています。
こうすることで、CounterView
やその子・孫Viewにおいて@EnvironmentObject var counter: Counter
と定義するだけで、このCounter
クラスのインスタンスを共有することができるようになります。
つまり、この例ではContentView
内でControlView
に対して明示的にself
やcounter
など親Viewの持つインスタンスを渡してないにも関わらず、ControlView
はcounter
の値を変更し、ContentView
に反映させることを可能にしています。
これを使うと、delegateを渡したり、シングルトンで管理する手間が省けるので大変便利なのですが、EnvironmentObjectはSceneを跨いで共有される(新規Sceneが生成される時、EnvironmentObjectは新しく生成されない)ので、マルチウインドウにしたとき状態が同期してしまって困ります。
そこで、ContentView
内ではCounter
のインスタンスを生成してStateObjectに入れて、それを子ViewのEnvironmentObjectに指定してやることで、Sceneごとに独立したインスタンスを使うことができるようになります。
import SwiftUI
@main
struct MultiWindowCounterApp: App {
var body: some Scene {
WindowGroup {
ContentView()
// ここでEnvironmentObjectの生成は不要
}
}
}
class Counter: ObservableObject {
@Published var count = 0
func increment() { count += 1 }
func reset() { count = 0 }
}
struct ContentView: View {
@StateObject var counter = Counter() // インスタンス生成
var body: some View {
VStack {
Text("\(counter.count)")
.padding()
ControlView()
}
.padding()
.environmentObject(counter) // 子のEnvironmentObjectにインスタンスを渡す
}
}
struct ControlView: View {
@EnvironmentObject var counter: Counter
var body: some View {
HStack {
Button("Increment") {
self.counter.increment()
}
Button("Reset") {
self.counter.reset()
}
}
}
}
期待通りの動作になりました。
EnvironmentObjectは使いこなせるとインスタンスの受け渡しを大幅に減らせるので大変便利です。ただ、SwiftUIで使われるProperty Wrapperは生成や破棄など、ライフサイクルをしっかり把握しておかないと期待通りに動作せずに困るので、理解しておきたいところですね。
コメント