SwiftUIのアーキテクチャについて調査〜SVVS編〜

# 本記事について

本記事では、SwiftUIのアーキテクチャについて調査するために、SVVSアーキテクチャを実装してみた感想をまとめました。個人的な意見であり、筆者の知識不足から誤った記述が含まれている可能性があるため、参考程度にご覧ください。

# SVVSとは？

このアーキテクチャを考案されたのはkoherさんChatworkさんで、今回はこちらの資料を参考にさせていただきました。

https://github.com/chatwork/svvs-sample

https://speakerdeck.com/ryunakayama/swiftuinishi-sitaxin-akitekutiyanodao-ru-nitiao-mu

SVVSの頭文字は、Store、View、ViewStateの略です。

Storeはアプリケーション全体の状態（state）を管理する役割を持ち、ReduxのStoreと似た役割を果たします。

Viewは、その名の通り、画面を表示する役割を担います。

ViewStateはプレゼンテーションロジックを持ち、主にViewの状態管理に関心を分離させています。

# 実装の解説

では、Qiitaの記事をリスト化するだけのシンプルなアプリをSVVSで作ってみます。

<img width=200 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3368292/2f3aa248-9c33-427c-2fd8-be0fdb32b718.png">

## Model関連

ここは本記事の肝ではないので、説明は割愛します。あくまでサンプルです。

```swift
struct Article: Decodable, Identifiable {
    let id: String
    let title: String
    let body: String
}

enum ArticleRepository {
    static func fetchArticles() async throws -> [Article] {
        let url = URL(string: "https://qiita.com/api/v2/items")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode([Article].self, from: data)
    }
}
```

## Store

記事（Article）の状態を管理するStoreを`@MainActor`で定義します。

`@Published`を付けたarticlesプロパティを保持し、データはRepositoryを通じて取得してarticlesに渡します。

また、状態の一元管理（Single Source of Truth）のために、このStoreはシングルトンとして実装します。

※DIは一旦置いておく

```swift
@MainActor
final class ArticleStore {
    static let shared: ArticleStore = .init()
    
    @Published private(set) var articles: [Article] = []

    func loadArticles() async throws {
        let articles = try await ArticleRepository.fetchArticles()
        self.articles = articles
    }
}
```

## ViewState

ViewStateを`@MainActor`で定義し、ObservableObjectに準拠させます。ViewStateにはarticlesプロパティを定義し、初期化時にstoreのarticlesプロパティを監視することで、値の変更を自動的に反映させます。

また、Viewのアクションに対応するためのメソッドも作成します。今回は画面表示のタイミングで処理を行うonAppear()メソッドを定義します。

```swift
@MainActor
final class ArticleViewState: ObservableObject {
    private let store: ArticleStore = .shared

    @Published private(set) var articles: [Article] = []

    private var cancellables = Set<AnyCancellable>()

    init() {
        store.$articles
            .assign(to: \.articles, on: self)
            .store(in: &cancellables)
    }

    func onAppear() async {
        do {
            try await store.loadArticles()
        } catch {
            print("Error: \(error)")
        }
    }
}
```

## View

Viewは、ViewStateを`@StateObject`を保持しています。

`.task`モディファイアを使用していますが、iOS 15.0+でしか使えないため、iOS15未満はonAppearを使います。

```swift
struct ArticleListView: View {
    @StateObject private var state: ArticleViewState

    init() {
        self._state = .init(wrappedValue: .init())
    }

    var body: some View {
        List(state.articles) { article in
            Text(article.title)
        }
        .task {
            await state.onAppear()
        }
    }
}
```

# 考えたこと

- 良い点
  - TCAに比べると学習コストが低くて、シンプル
  - [Observationの移行](https://developer.apple.com/documentation/swiftui/migrating-from-the-observable-object-protocol-to-the-observable-macro)が楽になる
- 対策したいこと
  - Combineの学習コストを減らしたい
  - ViewStateとViewModelとの差分がまだ理解できていないので、チームが迷うことなく実装できるように明確化したい
  - プレゼンテーションロジックのテストを書きやすくしたい（現時点で書くイメージができていないので今後も要調査）

# 最後に

SVVSとは何か？を焦点に当ててまとめました。
サンプルアプリがシンプルすぎたので、次回はもう少し複雑な画面で検証したいです。

同じくSwiftUIのアーキテクチャを検討している方の参考になれば幸いです。