本記事について
本記事では、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()
}
}
}
考えたこと
- 良い点
- TCAに比べると学習コストが低くて、シンプル
- Observationの移行が楽になる
- 対策したいこと
- Combineの学習コストを減らしたい
- ViewStateとViewModelとの差分がまだ理解できていないので、チームが迷うことなく実装できるように明確化したい
- プレゼンテーションロジックのテストを書きやすくしたい(現時点で書くイメージができていないので今後も要調査)
最後に
SVVSとは何か?を焦点に当ててまとめました。
サンプルアプリがシンプルすぎたので、次回はもう少し複雑な画面で検証したいです。
同じくSwiftUIのアーキテクチャを検討している方の参考になれば幸いです。