Flow - ObservationとSwift 6で実現するSwiftUI状態管理ライブラリ
Flowの紹介
SwiftUIアプリの状態管理、どのように実装していますか?シンプルさを保ちながら、型安全性と構造化を両立させるのは簡単ではありません。Flowは、ObservationとSwift 6 Concurrencyを活用し、この課題に一つの答えを提供します。
SwiftUIの状態管理における2つの課題 💭
課題1:シンプルさと構造化のバランス
シンプルな実装の限界:
-
@Stateだけでは複雑なビジネスロジックに対応しづらい - コードがView内に散在し、テストが困難
- 非同期処理やエラーハンドリングが煩雑に
構造化の代償:
- 構造を求めると、ボイラープレートが増加
- 学習コストが高くなる
- 小〜中規模のアプリには過剰な場合も
求められるもの:
シンプルさを保ちながら、適度な構造化を実現できないか?
課題2:ViewModelパターンでの非同期処理の複雑さ
SwiftUIでViewModelを使おうとした時の典型的な実装:
// ObservableObject + Combine時代のパターン
class UserViewModel: ObservableObject {
@Published var isLoading = false
@Published var users: [User] = []
@Published var errorMessage: String?
private let api: UserAPI
func loadUsers() {
isLoading = true
Task {
do {
let fetchedUsers = try await api.fetchUsers()
// MainActorへの明示的な切り替えが必要
DispatchQueue.main.async {
self.users = fetchedUsers
self.isLoading = false
}
} catch {
DispatchQueue.main.async {
self.errorMessage = error.localizedDescription
self.isLoading = false
}
}
}
}
}
この実装での困りごと:
-
DispatchQueue.main.asyncをあちこちに書く必要がある(書き忘れると警告やクラッシュの原因に) - エラーハンドリングのたびに同じパターンを繰り返す
-
isLoadingのセットし忘れでローディング状態が残り続ける - テストで非同期処理とUI更新のタイミングを検証するのが困難
求められるもの:
ViewModelパターンを使いつつ、MainActorの管理を自動化して、安全に非同期処理とState更新を書けないか?
Flowの答え ✨
Flowは、ObservationとSwift 6 Concurrencyを活用し、これらの課題に答えます:
問い:シンプルさと構造化の両立は?
→ 答え:単方向データフローとFeature単位の明確な構造、最小限のボイラープレート
問い:ViewModelでの非同期処理とMainActor管理は?
→ 答え:MainActor隔離により、非同期処理内で直接State変更が可能。Swift 6のコンパイル時チェックで、データ競合をコンパイル時に検出
Flowの5つのコア原則 🔍
1. 単方向データフロー:予測可能な状態管理
Flowは、ReduxやReSwiftに影響を受けた単方向データフローを採用しています:

流れの説明:
- View - ユーザーイベント(ボタンタップなど)が発生
-
Action - イベントを表すActionをStoreに送信(
store.send(.increment)) - Handler - ActionHandlerがActionを処理し、Stateを変更
-
State - Stateの変更がViewに自動的に反映(
@Observable) - View - UIが更新され、新しいStateを表示
// 1. Viewでイベント発生
Button("Load") {
store.send(.load) // 2. Actionを送信
}
// 3. HandlerでAction処理
ActionHandler { action, state in
switch action {
case .load:
state.isLoading = true // 4. State変更
return .run { state in
let data = try await api.fetch()
state.data = data // 4. State変更
}
}
}
// 5. Viewが自動更新(@Observableのおかげ)
if store.state.isLoading {
ProgressView()
}
メリット:
- 予測可能 - データの流れが一方向で追いやすい
- デバッグしやすい - どのActionがどのStateを変更したか明確
- テストしやすい - 入力(Action)と出力(State)が明確
2. ツリー構造はViewのみ:SwiftUIの哲学に沿った設計
SwiftUIでは、ツリー構造を持つのはViewだけです:
NavigationStack (View)
└─ ListScreen (View)
└─ DetailScreen (View)
親View → 子View → 孫Viewという階層はありますが、Stateは各Viewが@Stateでローカルに保持します。
多くの状態管理ライブラリの問題:
Storeもツリー構造にしようとします(親Store → 子Store → 孫Store)。これはSwiftUIの哲学から外れ、複雑さを増大させます。
Flowのアプローチ:
SwiftUIの標準に従い、各Viewが独立したStoreを持ちます。Store同士に親子関係はありません:
struct UserListView: View {
// このViewが独立したStoreを持つ
@State private var store = Store(
initialState: UserFeature.State(),
feature: UserFeature()
)
var body: some View {
List(store.state.users) { user in
Text(user.name)
}
.onAppear {
store.send(.load)
}
}
}
メリット:
- SwiftUIの標準に沿う - ツリー構造はViewのみ
- シンプル - Store階層の管理が不要
- ライフサイクルが明確 - StoreはViewと連動
- テストが独立 - 各Featureを個別にテスト可能
- メモリ効率的 - Viewが消えればStoreも解放
3. Result-Oriented:関数的な明快さ
struct TodoFeature: Feature {
@Observable
final class State {
var todos: [Todo] = []
}
enum Action: Sendable {
case save(title: String)
}
enum ActionResult: Sendable {
case saved(id: String)
}
func handle() -> ActionHandler<Action, State, ActionResult> {
ActionHandler { action, state in
switch action {
case .save(let title):
return .run { state in
let todo = try await api.create(title: title)
state.todos.append(todo)
return .saved(id: todo.id)
}
}
}
}
}
// View側
Button("Save") {
Task {
let result = await store.send(.save(title: title)).value
if case .success(.saved(let id)) = result {
await navigator.navigate(to: .detail(id: id))
}
}
}
メリット:
- アクションは値を返す - 関数的な明快さ
- 親Viewが制御できる - ナビゲーション、通知などを上位で決定
- 副作用の責任が明確 - どこで何が起きるかが追いやすい
- 型安全なコントラクト - 結果の型が明示的で、コンパイル時にチェック
4. MainActor隔離:非同期処理内での安全なState変更 ⭐️
Flowでは、非同期処理内で直接Stateを変更できます:
case .fetchUser:
state.isLoading = true
return .run { state in
// 非同期処理内でStateを直接変更!
let user = try await api.fetchUser()
state.user = user
state.isLoading = false
}
.catch { error, state in
state.isLoading = false
state.error = error
}
メリット:
- コードの局所性 - ローディング開始からエラーハンドリングまで1箇所に
- 直感的 - 通常のSwiftコードと同じ感覚で書ける
- コンパイル時安全性 - データ競合はコンパイルエラーで検出
5. SwiftUI標準のObservation
@Observable
final class State {
var count = 0
var isLoading = false
var errorMessage: String?
}
メリット:
- Combine依存なし - SwiftUIの標準機能のみ
- 最適化を享受 - SwiftUIの差分検知、パフォーマンス向上
- プラットフォームと協調 - Appleの進化と共に成長
- 学習コスト削減 - SwiftUI開発者にとって自然
おわりに 🌟
SwiftUIの状態管理は、ObservationとSwift 6 Concurrencyによって新しいステージに入りました。
Flowは、ObservationとSwift 6 Concurrencyを前提とすることで、シンプルで安全な設計を実現しました。特にMainActor隔離による非同期処理内での直接State変更は、従来の間接的なアプローチとは異なる特徴的な機能です。
Flowは以下のライブラリと思想から学びました:
- Redux - 単方向データフローの明快さ
- ReSwift - SwiftでのRedux実装の先駆者
- The Composable Architecture - 型安全な状態管理パターン
先人たちの知見の上に、Observation時代の新しいアプローチを構築しています。
Flowは、ObservationとSwift 6 Concurrencyを前提とした状態管理ライブラリです。
プロジェクトの要件、チームの特性、技術的な制約、将来のビジョンを考慮して、あなたのプロジェクトに合った選択をしてください。
次のステップ 🚀
Flowに興味を持っていただけましたか?
フィードバックや質問があれば、GitHubでお待ちしています。
この記事は2025年11月に執筆されました。技術情報は執筆時点のものです。
Discussion