Safie Engineers' Blog!

Safieのエンジニアが書くブログです

アーキテクチャをMVVMとEnvironment・Observableを使って実装してみた話

この記事はSafie Engineers' Blog! Advent Calendar 21日目の記事です

はじめに

Hello~ モバイルグループのアダムです。 今年も誕生日に記事を出せて嬉しいです!🎅

最近自分がはまっているモバイルのアーキテクチャの遊びについて紹介させてください!

今回の遊びの環境:
Xcode: 16.2
Minimum Target: 17.0
Swift: 6
Concurrency: Strict

MVVM

将来的に理想なアーキテクチャを行うならどんな感じで実装するのか?をやってみました。
今まで開発に参加していたアプリはほとんどMVVMだったので、最新のSwiftUIで実装した場合どんな形になるんだろう?🤔
オンメモリーのCacheを持つRepositoryがある場合どんな形で作るのか?

そのために今回どんな感じで考えたかを紹介させて〜

もしこんな方法おすすめ、こちらの方がベターっていうのがあったらこちら→@monolithic_adam or monolithic-adam.bsky.social にPOSTお願いします!

Environment

一旦例のためカウントを持つRepositoryとViewModelがincrementするUseCaseを用意します

Repository

@Observableにして何かの変更があった場合Viewが再描画される

@Observable
final class TestRepository {
    var count: Int = 0
}

ViewModel

ViewModelにシンプルなUseCaseを用意、Repositoryに最新の状態をアップデートできるfuncを用意します

struct TestViewModel {
    let repository: TestRepository
    
    func increment() {
        repository.count += 1
    }
}

App/Scope

今回のRepositoryがアプリスコープのため、Appの方でinit.environmentにセットする

@main
struct TestProjectApp: App {
    @State private var testRepository = TestRepository()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(testRepository)
        }
    }
}

View

こちらが迷いポイント1です!

RepositoryがEnvironmentの方で設定しているので、どうやってViewModelへ渡すのかを迷っています・・・
Environmentはinit時にまだ存在しないのでparentから渡すのか、ViewModelを.environmentにセットして、observeすべきかも?

ま、一旦parentから渡す感じで実装してみましょう!

struct ContentView: View {
    @Environment(TestRepository.self) var repository: TestRepository
    
    let viewModel: TestViewModel
    
    init(repository: TestRepository) {
        self.viewModel = TestViewModel(repository: repository)
    }
}

結局App側で二重に渡している感じになってしまう

@main
struct TestProjectApp: App {
    @State private var testRepository = TestRepository()
    
    var body: some Scene {
        WindowGroup {
            ContentView(repository: testRepository)
                .environment(testRepository)
        }
    }
}

最後に状態変化を見れるよう、シンプルにcountを表示させるTextとincrementできるボタンを用意して完成!

    var body: some View {
        VStack {
            Text("Current Count \(repository.count)")
            Button {
                viewModel.increment()
            } label: {
                Text("Increment")
            }
        }
        .padding()
    }

結果

まとめ

  • View -> ViewModelにRepository渡すことがEnvironmentをちゃんと活用できていないな〜
    • 画面遷移する時にどうせ次のビューを渡さないといけなくなるので、あまりEnvironmentにセットする意味がない
  • init時は気にしなくて良くなるViewModelをEnvironmentにした方が綺麗になる

理想はこうなるかな〜

おわりに

モバイルチームでは、このように多様な環境で世界に向けたアプリをともに開発する仲間を募集しています!
open.talentio.com

© Safie Inc.