1
3

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

Last updated at Posted at 2024-07-07

本記事について

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

SVVSとは?

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

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

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

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

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

実装の解説

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

Model関連

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

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は一旦置いておく

@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()メソッドを定義します。

@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を使います。

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()
        }
    }
}

考えたこと

  • 良い点
  • 対策したいこと
    • Combineの学習コストを減らしたい
    • ViewStateとViewModelとの差分がまだ理解できていないので、チームが迷うことなく実装できるように明確化したい
    • プレゼンテーションロジックのテストを書きやすくしたい(現時点で書くイメージができていないので今後も要調査)

最後に

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

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

1
3
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
1
3