16
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【SwiftUI】@EnvironmentObjectはシングルトンにした方が良い話

Posted at

おはこんばんにちは。
気づいたらアプリエンジニアに転身していた和尚です。

今回は私がSwiftUIアプリを作ってきたなかで、ViewのProperty Wrapperである「EnvironmentObject」はシングルトンにした方がいいなと思ったので、記事を書いていきたいと思います。

概要

SwiftUIのViewModelには、「StateObject」や「ObservedObject」といったProperty Wrapperを用いてViewと1対1の構造をとるViewModelと、「EnvironmentObject」という複数のView間で値を共有することのできるグローバルなViewModelの大きく2種類があります。(解説のため、前者を「ViewModel」、後者を「GlobalViewModel」と呼びます

EnvironmentObjectについては詳しくは説明はしませんが、MVVMアーキテクチャを採用する場合はビジネスロジックはこのEnvironmentObjectを含めた全てのViewModelに記載していきます。

基本的にはSwiftUIでアプリを作成する場合、あるViewに対してのビジネスロジックはそのあるViewに紐づいたViewModelに書いていくことになります。ただ複数のViewを跨いで値を共有したい場合はGlobalViewModelを用いることになります。そういった作りをしていく中でも、ViewModelからGlobalViewModelのプロパティを弄ったり、関数を呼び出したい時があります。本件はその課題を解決するための記事となります。

解決したい課題

  • ViewModelからGlobalViewModelのプロパティを変更したい
  • ViewModelからGlobalViewModelの関数を呼び出したい

これまでの自分の解決法

このような場合、自分はGlobalViewModelのインスタンスをViewからViewModelに逐一渡していました。

GlobalViewModel.swift
final class GlobalViewModel: ObservableObject {
    @Published var count: Int = .zero
    
    func countUp() {
        count += 1
    }
}
MainApp.swift
@main
struct MainAppApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView().environmentObject(GlobalViewModel())
        }
    }
}
ContentViewModel.swift
final class ContentViewModel: ObservableObject {
    func onTapButton(_ globalViewModel: GlobalViewModel) {
        globalViewModel.countUp()
    }
}
ContentView.swift
struct ContentView: View {
    @EnvironmentObject private var globalViewModel: GlobalViewModel
    @StateObject private var viewModel = ContentViewModel()
    
    var body: some View {
        VStack {
            Text("\(globalViewModel.count)")
            Button(action: {
                viewModel.onTapButton(globalViewModel)
            }) {
                Text("BUTTON")
            }
        }
        
    }
}

この実装方法の場合引数が多くなってしまい大変なのと、そもそもこのGlobalViewModelという存在自体が擬似シングルトンのような動きをしているため、だったら最初からシングルトンにしてしまえば良いのではないかと考えた結果以下のような実装に落ち着きました。

GlobalViewModelをシングルトンにした書き方

GlobalViewModel.swift
final class GlobalViewModel: ObservableObject {
    static let shared: GlobalViewModel = .init() // シングルトンクラスへ
    private init() {}
    
    @Published var count: Int = .zero
    
    func countUp() {
        count += 1
    }
}
MainApp.swift
@main
struct SampleAppApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView().environmentObject(GlobalViewModel.shared)
        }
    }
}
ContentViewModel.swift
final class ContentViewModel: ObservableObject {
    func onTapButton() {
        GlobalViewModel.shared.countUp()
    }
}
ContentView.swift
struct ContentView: View {
    @EnvironmentObject private var globalViewModel: GlobalViewModel
    @StateObject private var viewModel = ContentViewModel()
    
    var body: some View {
        VStack {
            Text("\(globalViewModel.count)")
            Button(action: viewModel.onTapButton) {
                Text("BUTTON")
            }
        }
    }
}

GlobalViewModelのインスタンスをViewから渡す必要がなくなったので、かなりContentViewModelがスッキリしましたね。

注意点

GlobalViewModelをシングルトン化したことで、これまで.sheet.fullScreenCoverのViewに対しては.environtmentObject()でGlobalViewModelを設定しなければなりませんでしたが、設定しなくてもプロパティが共有されるようになりました。

またこれまでGlobalViewModelは.environmentObject()で設定したViewより下の階層のViewでのみ適用されていましたが、それも全部で共有されるようになるので扱いには注意が必要です。

後書き

いかがだったでしょうか。
既存のシステムにこの修正を加える場合は注意書きに書いたものを考慮する必要がありますが、ViewModel内からGlobalViewModelに直接アクセスできるためViewおよびViewModelがとてもスッキリするのでオススメです。

ではでは!

16
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?