【SwiftUI】Singleton と EnviromentObject

画面遷移をして、他の View から View に移動した際にそれぞれの View から参照されたりしてアプリケーションの中で複数の状態を持って欲しく無い様なグローバルな値を使いたい事はなんやかんやあると思います。 SwiftUI では現状その様な変数へのアプローチとして Singleton と EnviromentObject が使えると思います。 Singleton は別に目新しいものでもありませんが SwiftUI ではこんな感じで使えるという点を紹介します。

Singleton

今回やりたかったのは以下の様に NavigationView で遷移した先から何らかの条件でブーリアンを返す isVisible() を呼んで出たり消えたりさせたいって感じでした。これは単純化したために @State で宣言したフラグを次の画面で Binding して切り替えれば良いのですが分かりやすくするために今回はこれで行きます。

VStack{
            NavigationView {
                VStack{ 
                    Text("ページ 1")
                        .bold()
                    
                    NavigationLink(destination: SecondPage()) {
                        Image(systemName:"arrowshape.turn.up.right.circle")
                    }
                }
            }
            
            if (isVisible()) {
                Image("hoge")
                    .frame(width: 100, height: 100, alignment: .center)
            }

        }

まず1つだけしかインスタンスが作られない Singleton のクラスを作ります。これを ObservableObject として各 View でこの Published の変数が変化した際に View が更新される様にします。

class SingletonClass: ObservableObject {
    
    static var shared = SingletonClass()
    
    @Published var visibleFlag: Bool = false
    
    func isVisible() {
        visibleFlag = Bool.random()
    }
}

次にこいつを使ってさっきの View を書き換えていきます。

struct FirstPage: View {
    
    @ObservedObject private var singletonClass = SingletonClass.shared
    
    var body: some View {
        VStack{
            
            NavigationView {
                VStack{  
                    Text("ページ 1")
                        .bold()
                    
                    NavigationLink(destination: SecondPage()) {
                                    
                        Image(systemName:"arrowshape.turn.up.right.circle")
                    }
                }
            }
            
            if (singletonClass.visibleFlag) {
                Image("hoge")
                    .frame(width: 100, height: 100, alignment: .center)
            }
        }
    }
}

最後に遷移先のページで SingletonClass のメソッドを呼びます。

struct SecondPage: View {
        
    var body: some View {
        VStack{
            NavigationView {
                VStack {
                    Text("ページ 2")
                        .bold()
                    Button(action: {
                        SingletonClass.shared.isVisible()
                    }){
                        Text("クリック")
                    }
                }
            }
        }
    }
}

こんな感じの流れです。

EnviromentObject

さっきと全く同じ挙動を EnvironmentObject で実装すると以下の様になります。

class EnvironmentClass: ObservableObject {
    
    @Published var visibleFlag: Bool = false
    
    func isVisible() {
        let result = Bool.random()
        print("result", result)
        visibleFlag = result
    }
}

こちらを FirstPage を呼ぶ際にインジェクトします。(インジェクトという表現が合ってるかは知りませんがなんか DI ぽい感じなんで)

FirstPage()
       .environmentObject(EnvironmentClass())

あとは EnvironmentClass を使って以下の様に実装します。


struct FirstPage: View {
    
    @EnvironmentObject private var environment :EnvironmentClass
    
    var body: some View {
        VStack{      
            NavigationView {
                VStack{
                    Text("ページ 1")
                        .bold()
                    
                    NavigationLink(destination: SecondPage()) {
                                    
                        Image(systemName:"arrowshape.turn.up.right.circle")
                    }
                }
            }
            
            if (environment.visibleFlag) {
                Image("hoge")
                    .frame(width: 100, height: 100, alignment: .center)
            }

        }
    }
}
struct SecondPage: View {
    
    @EnvironmentObject private var environment :EnvironmentClass
    
    var body: some View {
        VStack{
            NavigationView {
                VStack {
                    Text("ページ 2")
                        .bold()
                    Button(action: {
                        environment.isVisible()
                    }){
                        Text("クリック")
                    }
                }
            }
        }
    }
}

動作はこんな感じになります。

使い分けについて

こちらの stack overflow では EnvironmentObject vs Singleton in SwiftUI? という事で 別に EnvironmentObject なんて使わなくていいじゃん、前からある Singleton でよくね? って質問でした。

回答は、そうだね Apple も何でもかんでも EnvironmentObjects 使えとは言ってないし Singleton でできるならいいと思うよ、ましてや結構 @State and @Binding で済む場合が多いよ(本記事の例は正にそうですね)といったものでした。

しかしEnvironmentObject も(@State and @Binding みたいに) 値の受け渡ししなくていいし便利だよとも書いてありましたが、これってやっぱ Singleton パターンでやればいいし結局そんな違いは無い?という疑問は残りました。

もう少し他の記事とかを見てみると

https://mokacoding.com/blog/swiftui-dependency-injection/

これとか

https://cockscomb.hatenablog.com/entry/swiftui-environment-di

これでは EnvironmentObject やはり DI として使えるのがメンテナンス性とかの面で良いのでは、と言った感じで紹介されていました。

違いといえばそんな感じですかね。詳しい方は教えて頂きたいです。

学習書籍


詳細! SwiftUI iPhoneアプリ開発入門ノート iOS 13 + Xcode11対応


詳細! SwiftUI iPhoneアプリ開発入門ノート[2020] iOS 14+Xcode 12対応

SwiftUI 徹底入門


1人でアプリを作る人を支えるSwiftUI開発レシピ (技術の泉シリーズ(NextPublishing))


基礎から学ぶ SwiftUI