Ideal Reality

興味の赴くままに

[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に対して明示的にselfcounterなど親Viewの持つインスタンスを渡してないにも関わらず、ControlViewcounterの値を変更し、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は生成や破棄など、ライフサイクルをしっかり把握しておかないと期待通りに動作せずに困るので、理解しておきたいところですね。

スポンサーリンク

コメント

投稿されたコメントはありません

名前

メールアドレス(任意)

コメント

関連する投稿

[iOS, Swift]で画面回転時のアニメーションを無効化する

[SwiftUI]List内のButtonやLinkのデザインをNavigationLinkっぽくする方法

[SwiftUI]Apple Watchで画面遷移から戻った際にアニメーションが崩壊するのを防ぐ

NO IMAGE

SwiftUIでUISwipeGestureRecognizerを使ってスワイプを検出する

‘configure(withApplicationID:)’ is deprecatedと言われた

SwiftUIでステータスバーの色を変える方法4つ