[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は生成や破棄など、ライフサイクルをしっかり把握しておかないと期待通りに動作せずに困るので、理解しておきたいところですね。
コメント