0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Swift 6 で Redux と Redux Saga を作り直した

0
Posted at

以前、Redux Saga for Swift という記事を書きました。

その記事で、ReSwift1 の上で動く Redux Saga の自作ライブラリ ReSwift-Saga2 を紹介しました。そのライブラリは機能も少なく、エラー処理やテストも今後の課題として残したままでした。やり残しを抱えたまま、気づけば Swift 6 の時代です。

Swift 6 では、非同期処理まわりが大きく強化されました。async / await と構造化並行性で、非同期の処理を上から順に書けて、キャンセルも親から子へ伝わります。さらに strict concurrency によって、スレッドをまたいで受け渡す値が Sendable かどうかをコンパイラが検査するようになりました。

Saga がしていることを並べてみると、Action を待ち、処理をして、結果を Action で返し、ときには処理をキャンセルしたり並行に動かしたりします。受け渡すのは値としての Action と State です。どれも Swift 6 が得意とする領域です。

というわけで、Redux 本体も含めて Swift 6 向けにまるごと作り直したのが swift-redux-saga3 です。企画と設計は私、設計補助と主な実装は Claude Code、品質管理と実装少々は Codex の共作した個人の趣味ライブラリです。Swift 6 ならいいものが作れそうと思って、AI で実現させました。今は 0.2.1 を公開していて、Swift Package Manager で導入できます。

この記事では、Redux と Redux Saga のおさらいをしてから、なぜ今 Swift で Redux なのか、他のライブラリはどういう状況なのかを整理し、swift-redux-saga の使い方を紹介します。ライブラリの内部実装の話はしませんが、興味あれば、ソースコードを見てください。

Redux そして Redux Saga へ

簡単に Redux というアーキテクチャについて説明します。

Redux

Redux4 は、JavaScript で生まれた状態管理のための予測可能な状態コンテナです。アプリの状態を 1 か所にまとめ、状態の変更を決まった経路に限定することで、状態がいつ、なぜ変わったのかを追えるようにします。

登場するのは次の部品です。

  • State
    • アプリの状態を表す値です
    • 画面に表示するデータはすべてここから読みます
  • Action
    • 「ボタンが押された」「データが届いた」のような出来事を表す値です
  • Reducer
    • 今の State と Action から次の State を作る関数です
    • 副作用(通信など)は持ちません
  • Store
    • State を 1 つだけ持ち、受け取った Action を Reducer に通して State を更新します

View は State を読んで表示し、ユーザーの操作を Action として Store に送ります(dispatch)。State を書き換えられるのは Reducer だけなので、State の変化はすべて Action の列で説明できます。データが一方向にしか流れないことが、Redux のいちばん大事な性質です。開発において重要な考えである Single source of truth(信頼できる唯一の情報源)が実現されます。

Redux Toolkit

素の Redux で書くと、Action の型の定数、Action を作る関数、Reducer の switch 文と、同じ機能のためのコードがあちこちに分かれ、量も増えがちでした。この手間を減らすために Redux 公式が出しているのが Redux Toolkit5(RTK)で、今は Redux のコードを書くときの公式の推奨になっています。

中心になるのは createSlice です。1 つの機能の State の初期値と、Action ごとの State の更新処理をまとめて書くと、Reducer と Action を作る関数がいっしょに生成されます。ほかにも、State から表示用の値を計算して結果をキャッシュする createSelector、ID で引ける一覧を扱う createEntityAdapter などがそろっています。swift-redux-saga の Redux 本体は、この Redux Toolkit を手本にしたものです。

Redux Saga

Redux はシンプルである一方、通信のような副作用をどこに書くのかは決めていません。Reducer は純粋な関数でなければならないので通信はできませんし、View で通信してから結果を dispatch すると、ロジックが View に散らばります。

そこで登場するのが Redux Saga です。Saga はアプリの中で副作用を個別に実行する独立したスレッドのようなイメージです。Saga は Store の外側で Action を待ち受け、Action が届いたら通信などの処理をして、結果を新しい Action として Store に戻します。View は Action を送るだけ、Reducer は State を作るだけ、Saga は副作用だけ、と役割がきれいに分かれます。

一般的に副作用の実装はアーキテクチャにロックインされてしまいます。しかしながら、Redux Saga は副作用を関数で定義するので、他のアーキテクチャへの移植性は比較的高いというメリットもあります。

Action が Middleware と Reducer を通って Store の State になり、View が State を表示する。Saga は Middleware から Action を受け取り、API などを呼んで、結果の Action を put で戻す

実線が Redux の通常の流れで、Action は一直線に Reducer まで進みます。点線が Saga の流れで、Saga は通常の流れの外側で Action を受け取り、処理の結果を流れの先頭に戻します。

Saga の中では、take(Action を待つ)、put(Action を発行する)、call(関数を呼ぶ)、fork(別の Saga を並行して動かす)といった命令を組み合わせて処理を書き、これらの命令は Effect と呼ばれています。よく使う組み合わせはヘルパーとして用意されていて、たとえば takeLatest は、Action が届くたびに処理を始め、前の処理が終わっていなければキャンセルします。検索欄の入力のように、古い結果が要らなくなる処理を短く書けます。

なぜ今 Swift で Redux なのか

Swift で Redux を使う試みは以前からあり、ReSwift はその代表です。それでも今あらためて作り直したのは、Swift の言語とフレームワークの側が、ここ数年で Redux と相性のよい形に変わってきたためです。

Swift Concurrency

redux-saga は JavaScript のジェネレーター関数(function* と yield)を使って、非同期の処理を上から順に書けるようにしています。Swift にジェネレーターはありませんが、async / await があります。yield take(...) を try await ctx.take(...) と書けば、ほぼ同じ感覚で読めるはずです。

ここで、Swift では AsyncStream の continuation.yield(_:) のように yield という名前は出てきます。しかし、これは非同期のシーケンスに値を流すメソッドで、関数を途中で止めて値を返すジェネレーターの yield とは別物です。

さらに、Swift の構造化並行性では、親のタスクをキャンセルすると子のタスクにもキャンセルが伝わります。fork した Saga を親ごと止める、takeLatest で古い処理を止める、といった redux-saga のキャンセルの考え方を、言語の仕組みのまま実現できます。

Swift 6 の strict concurrency

Swift 66 からは、スレッドをまたいで受け渡す値が安全かどうか(Sendable かどうか)をコンパイラが検査します。共有された可変の状態を持つ設計ほど、この検査を通すのに苦労します。

一方、Redux の State は不変の値として受け渡され、変更は Reducer の中だけで起き、Action もただの値です。Swift の struct と enum で書けば、State と Action は自然に Sendable になります。Redux の「値と純粋関数で状態を扱う」という考え方は、Swift 6 が求める書き方と同じ方向を向いています。

SwiftUI と Observation

SwiftUI は、React と同じく「状態から画面を作る」宣言的な UI です。そして iOS 17 から使える Observation(@Observable)で、View が読んだプロパティが変わったときだけ再描画されるようになりました。

ReSwift の頃は、StoreSubscriber に準拠して購読し、newState(state:) で受け取り、同じ値なら無視する設定をして……と手間がかかりました。今は View の中で store.count と書くだけで済みます。UIKit でも、iOS 26 からは Observation による自動追跡が既定で有効になりました。

他のライブラリの開発状況

以下は 2026 年 10 月時点で、各リポジトリを確認した内容です。

ReSwift

ReSwift は、Swift で Redux を使うライブラリとして長く使われてきました。ただ、最新のリリースは 2023 年 1 月の 6.1.1 で、最後のコミットは 2024 年 4 月の README の更新です。Package.swift の swift-tools-version は 5.7 のままで、ソースに Sendable は出てきません。Swift 6 言語モードのアプリから使うと、Store や Action の受け渡しが安全であることを、使う側で保証しなければなりません。非同期処理の拡張である ReSwift-Thunk7 も、最新のリリースは 2020 年 10 月の 2.0.1 です。

私の旧ライブラリ ReSwift-Saga は ReSwift の上で動いていたので、ReSwift ごと置き換えることにしました。

The Composable Architecture

The Composable Architecture8(TCA)は、Point-Free が開発している、Redux の考え方を取り入れたアーキテクチャです。開発は活発で、2026 年 8 月に 1.26.2 が出ています。

TCA は状態管理だけでなく、依存性の注入、画面遷移、テストまでを一体で提供するフレームワークで、swift-dependencies や swift-case-paths など Point-Free の多くのパッケージに依存しています。完成度は高いのですが、アプリの書き方全体を TCA に合わせることになります。副作用も TCA 独自の Effect で書くため、redux-saga とは書き方がだいぶ違うものになります。

ReactorKit

ReactorKit9 も、Action を受けて State を作る単方向の設計を取るライブラリです。2026 年もコミットは続いていますが、最新のリリースは 2022 年 1 月の 3.2.0 で、RxSwift に依存しています。Swift Concurrency と Observation を前提にした今の SwiftUI の書き方とは、組み合わせにくい面があります。

JavaScript の redux-saga

本家の redux-saga10 も今もメンテナンスされていて、2026 年 7 月に 1.5.1 が出ています。ただし、Redux 公式のドキュメント11は、データの取得には Saga ではなく RTK Query を、Action への反応には listener middleware を勧めていて、Saga は listener で足りないときに使うもの、という位置づけです。一方で、Saga は「バックグラウンドスレッドのような振る舞いや、debounce やキャンセルを必要とする、とても複雑な非同期のワークフロー」に向いている、とも書かれています。

私もこの評価には納得していて、通信して結果を表示するだけの処理に Saga は大げさだと思います。それでも、ログインの間だけ同期を動かす、入力が止まってから検索する、保存の順番を守る、といった時間の絡む処理は、モバイルアプリでは日常的に出てくるものです。こうした処理を読みやすく書くために、Swift にも Saga があってよいと考えています。

Swift 6 対応の Redux と Saga はなかった

Swift 6 にきちんと対応した軽い Redux と、redux-saga と同じ書き方で副作用を書ける仕組みの両方を満たすライブラリは、見つかりませんでした。それなら自分で作るしかありません。

swift-redux-saga を使おう

swift-redux-saga は、Swift 6 言語モードで警告なしにビルドできる Redux と Redux Saga の Swift Package です。Redux 本体は前に紹介した Redux Toolkit を参考にしていて、createSlice や createSelector に当たる機能もあります。マクロ用の swift-syntax を除いて外部のライブラリには依存していません。動作環境は Swift 6.2(Xcode 26)以降、iOS 17 以降です。

導入は Package.swift か、Xcode の File > Add Package Dependencies から行います。

dependencies: [
  .package(url: "https://github.com/mitsuharu/swift-redux-saga.git", from: "0.2.1")
]

Redux と Saga を使うなら ReduxSaga を、SwiftUI の補助も使うなら ReduxSwiftUI をターゲットに追加します。Saga だけを他の状態管理と組み合わせても使えます。

アーキテクチャにロックインされないように作る

swift-redux-saga を作るときにいちばん意識したのは、アプリがこのライブラリにロックインされないことです。状態管理のライブラリは、一度アプリ全体に広げると簡単には外せません。メンテナンスが止まったり、Swift の大きな変更に追いつけなくなったりすると、アプリごと身動きが取れなくなります。ReSwift の上に ReSwift-Saga を載せていた私自身が、Swift 6 を前にしてまさにその状況になりました。旧ライブラリの記事でもアーキテクチャとの接点を小さくする方針を書きましたが、今回はそれを設計の前提に据えました。

そのために、通信や保存などのビジネスロジック(UseCase や Repository)は、このライブラリを import せずに書けるようにしました。Saga は「Action を受けて、ビジネスロジックを呼び、結果を Action で返す」だけの薄い層にとどめます。Saga から呼ぶ call は、Action を受け取る関数に限らず、どんな async 関数でも呼べます。ビジネスロジックが必要とする依存は、Saga を作るときに引数で渡す形です。こうしておけば、ビジネスロジックの側は Action も State も知らずに済みます。

サンプルアプリは、この方針に沿って 3 つのターゲットに分けています。

Domain       ライブラリに依存しない(Model / Repository / UseCase)
AppFeature   Domain + swift-redux-saga(State / Action / Reducer / Saga)
App          SwiftUI や UIKit の画面と、Store の組み立て

ライブラリを入れ替えることになっても、書き直すのは AppFeature の State・Action・Reducer・Saga だけで、Domain はそのまま使えます。AppFeature は UI のフレームワークにも依存しないので、Saga や Reducer のテストはアプリを起動せずに実行できます。小さなアプリなら 1 つのターゲットにまとめてもかまいません。それでも分けておけば、ロジックが View に紛れ込みにくくなるという利点もあります。

ライブラリ自体も、使う範囲を選べるように分けています。Saga は Redux 本体に依存しておらず、Saga のプロダクトだけを追加すれば、他の状態管理と組み合わせて使えます。画面側も、Store を直接読んでもよいし、MVVM の ViewModel を挟んでもよく、どちらかに決め打ちしていません。

カウンターを作る

まずは定番の、ボタンでカウントを増やし、「読み込み」で値をサーバーから取ってくるカウンターです。

State と Action と Reducer は、Slice という入れ物にまとめます。Redux Toolkit の createSlice に当たるものです。

import Redux

enum Counter: Slice {
  struct State: Sendable, Equatable {
    var count = 0
    var isLoading = false
  }

  enum Action: Sendable, Equatable {
    case increment
    case fetch
    case fetched(Int)
  }

  static let initialState = State()

  static func reduce(into state: inout State, action: Action) {
    switch action {
    case .increment: state.count += 1
    case .fetch: state.isLoading = true
    case .fetched(let value):
      state.isLoading = false
      state.count = value
    }
  }
}

JavaScript の Reducer は新しい State を返しますが、ここでは inout で受け取った State を直接書き換えます。Swift の struct は値型なので、書き換えても呼び出し元の State が勝手に変わることはなく、Reducer は純粋な関数のままです。Action を enum にしておけば、switch で扱い忘れた case がコンパイルエラーになるのも嬉しいところです。

ReSwift を使ったことがある方は、ここで少し書き方が違うと感じるかもしれません。swift-redux-saga の Redux 本体は ReSwift ではなく Redux Toolkit を手本にしているためです。ReSwift では、Action は Action プロトコルに準拠した struct で、Reducer は Optional の State を受け取って新しい State を返す関数でした。

// ReSwift の書き方
struct IncrementCounter: Action {}

func counterReducer(action: Action, state: CounterState?) -> CounterState {
  var state = state ?? CounterState()
  switch action {
  case _ as IncrementCounter: state.count += 1
  default: break
  }
  return state
}

swift-redux-saga では、Action を enum にし、機能ごとの State・Action・Reducer を Slice にまとめます。初期値は initialState として別に持つので、Reducer が Optional の State を扱うこともありません。複数の Slice は Reducer.slice でアプリ全体の Reducer にまとめます。Redux Toolkit の createSlice を使ったことがあれば、こちらのほうが見慣れた形だと思います。State の変化の受け取り方も違い、ReSwift の StoreSubscriber の代わりに、後で紹介する Observation で読むだけです。ReSwift からの書き換え方は、移行ガイドに対応表を載せています。

次に Saga を書きます。前の節で書いたとおり、通信の処理そのものはライブラリを import しない普通の Swift のコードとして用意し、Saga を作るときに渡します。

// ライブラリを import しない
struct CounterRepository: Sendable {
  let load: @Sendable () async throws -> Int
}

extension CounterRepository {
  // 本番の実装(fetchCountFromServer はアプリの通信処理)。テストでは別の実装を渡す。
  static let live = CounterRepository { try await fetchCountFromServer() }
}
import Saga

struct CounterSagas: Sendable {
  let repository: CounterRepository

  var root: Saga<Counter.State, Counter.Action> {
    Saga("counter") { ctx in
      ctx.takeLatest(.action(.fetch)) { ctx, _ in
        do {
          let value = try await ctx.call(repository.load)
          await ctx.put(.fetched(value))
        } catch {
          // 失敗も Action にして返す
        }
      }
    }
  }
}

Effect は ctx のメソッドとして呼び、redux-saga で yield を書く場所に await を書きます。この Saga は .fetch が届くたびに通信し、結果を .fetched として Store に戻すものです。通信中にもう一度 .fetch が届けば、takeLatest が前の通信をキャンセルします。redux-saga の takeLatest(FETCH, fetchCount) とほぼ同じ見た目ですね。

Store は、Saga を Store につなぐ SagaMiddleware を渡して作り、作った後で Saga を起動します。

import ReduxSaga

@MainActor
func makeStore() -> Store<Counter.State, Counter.Action> {
  let sagaMiddleware = SagaMiddleware<Counter.State, Counter.Action>()
  let store = Store(
    initialState: Counter.initialState,
    reducer: Counter.reducer,
    middleware: [sagaMiddleware]
  )
  sagaMiddleware.run(CounterSagas(repository: .live).root)
  return store
}

SwiftUI の View は、State のプロパティを読み、Action を dispatch するだけです。

import ReduxSwiftUI

struct CounterView: View {
  @Environment(Store<Counter.State, Counter.Action>.self) private var store

  var body: some View {
    VStack {
      Text("\(store.count)")
      Button("+1") { store.dispatch(.increment) }
      Button("読み込み") { store.dispatch(.fetch) }
        .disabled(store.isLoading)
    }
  }
}

store.count を読んだ View は、count が変わったときだけ再描画されます。購読の登録や解除は書きません。ReSwift の StoreSubscriber を書いていた頃と比べると、ずいぶん短くなりました。

ログインと ToDo を組み合わせる

リポジトリの Examples/ には、ログインと ToDo の 2 つの機能を持つサンプルアプリが、SwiftUI 版と UIKit 版の両方で入っています。ここからはその中身を紹介します。

ログインの Saga は次のとおりです。ログインのボタンが押されたら通信し、成功すれば .loggedIn、失敗すれば .failed を返します。

public var root: Saga<AuthFeature.State, AuthFeature.Action> {
  Saga("auth") { ctx in
    ctx.takeLeading(.case(\.loginTapped)) { ctx, name in
      do {
        guard let user = try await ctx.call(useCase.login, name) else {
          await ctx.put(.failed("Enter your name."))
          return
        }
        await ctx.put(.loggedIn(user))
      } catch is CancellationError {
      } catch {
        await ctx.put(.failed(error.localizedDescription))
      }
    }
  }
}

takeLeading は、処理中に届いた同じ Action を無視するヘルパーで、ボタンの連打対策に使っています。.case(\.loginTapped) は、loginTapped(name:) という case のときだけ反応して、関連値の name を取り出して渡す、という意味です。enum の case から値を取り出すコードは、@Slice や @ActionCases というマクロで生成しています。

アプリ全体の State は、機能ごとの State をプロパティとして並べただけの struct です。ログインの Saga はログインの State と Action の型のまま書き、親につなぐときに型を付け替えます。そのため、機能ごとのコードはアプリ全体の型を知らずに書けます。

Saga がいちばん役に立つのは、機能をまたぐ流れを書くときです。サンプルでは、ToDo の同期をログインしている間だけ動かしています。ルートの Saga を簡略化すると、次のようになります。

while true {
  _ = try await ctx.take(.case(\.auth?.loggedIn))
  let session = ctx.fork(todoSagas.root, state: \.todo, action: \.todo, embed: AppAction.todo)
  _ = try await ctx.take(.case(\.auth?.loggedOut))
  ctx.cancel(session)
}

はい、無限ループです。ログインを待ち、ToDo の Saga を動かし、ログアウトを待って止める、という流れがそのままコードになっています。ctx.cancel(session) で止めると、ToDo の Saga が実行中だった読み込みや保存もまとめてキャンセルされます。同じことを ViewModel のフラグや Combine の購読で書くと、どこで何を止めるかのコードがあちこちに散らばりがちでした。

ToDo の Saga では、検索語の入力が 300 ミリ秒止まってから反映する debounce や、保存の順番を守るために Action を一度溜めておく actionChannel も使っています。Effect の一覧は、redux-saga との対応表も含めて Saga の Effect にまとめています。

SwiftUI では useSelector のように読む

React Redux を使ったことがある方なら、useSelector と useDispatch で、読みたい値と送る関数だけを宣言する書き方に馴染みがあると思います。swift-redux-saga では、@SelectState と @DispatchAction というプロパティラッパーで同じように書けます。次のコードはサンプルのログイン画面です。

struct LoginView: View {
  @SelectState(\RootFeature.State.auth.isLoggingIn) private var isLoggingIn
  @SelectState(\RootFeature.State.auth.errorMessage) private var errorMessage
  @DispatchAction private var dispatch: (RootFeature.Action) -> Void
  @State private var name = ""

  var body: some View {
    Form {
      TextField("Name", text: $name)
      Button("Log in") { dispatch(.auth(.loginTapped(name: name))) }
        .disabled(isLoggingIn)
      if let errorMessage {
        Text(errorMessage).foregroundStyle(.red)
      }
    }
  }
}

View は Store の型を知らず、読む値と送る Action だけを知っています。@SelectState は読んだ値が変わったときだけ再描画するので、ToDo の一覧が更新されてもこの画面は描き直されません。入力中の名前は画面の中でしか使わないので、View の @State に置きました。

画面の状態をすべて Store に入れる必要はありません。サンプルでは、設定画面のように Store の値を表示して操作するだけの画面は Store を直接使い、入力中の文字列など画面特有の状態がある ToDo の画面は MVVM を経由しています。ViewModel は Store の値の写しを持たず、計算プロパティで Store を読むので、データは Store の 1 か所にだけあります。

テストは結果を確かめる

redux-saga では、ジェネレーターから Effect を 1 つずつ取り出して、期待どおりの Effect かを確かめるテストがよく書かれます。残念ながら Swift にジェネレーターはないので、この書き方はできません。代わりに swift-redux-saga では、Saga を実際に動かし、発行された Action と State の変化を確かめます。サンプルのテストから、ログインすると ToDo が読み込まれることを確かめる部分を抜き出します。

@Test func todosAreLoadedOnlyAfterLoggingIn() async throws {
  let store = makeStore()

  try await store.send(.auth(.loginTapped(name: "me"))) { $0.auth.isLoggingIn = true }
  try store.receive(.auth(.loggedIn(User(name: "me")))) {
    $0.auth.isLoggingIn = false
    $0.auth.user = User(name: "me")
  }
  try store.receive(.todo(.refresh)) { $0.todo.isLoading = true }
  try store.receive(.todo(.loaded([milk], generation: 0))) {
    $0.todo.isLoading = false
    $0.todo.todos.ids = [milk.id]
    $0.todo.todos.entities = [milk.id: milk]
  }
  try await store.finish()
}

send で Action を送り、receive で Saga が発行した Action を順に確かめます。後ろのクロージャには、期待する State の変化を書きます。TestStore はすべての Saga が次の Effect で止まるまで待ってから確かめるので、sleep で待つ必要がなく、マシンの速さでテストの結果が変わりません。debounce のような時間の絡む処理は、TestClock で時間を進めて確かめます。

結果を確かめるテストは、Saga の中の書き方を変えても、振る舞いが同じなら壊れません。Redux 公式のドキュメントは、Saga のテストが実装の細部に依存しやすいことを欠点に挙げていますが、この書き方ならその心配は小さくなります。旧ライブラリでやり残していたテストも、これでようやく書けるようになりました。

評価と考察

サンプルアプリを作ってみていちばんよかったのは、時間の絡む処理を読めるコードで書けたことです。ログイン中だけ同期を動かす処理も、入力が止まってから検索する処理も、Saga を開けば流れが上から順に書いてあります。ViewModel に書いていた頃のように、フラグやタスクの参照をいくつも持ち回る必要はありません。キャンセルが親から子へ伝わるので、止め忘れも起きにくくなりました。View の処理も Action を送るだけになり、とてもシンプルになって満足しています。

Swift らしく書けるのも気に入っています。Action を enum にすれば Reducer の扱い忘れはコンパイルエラーになり、State と Action は Sendable な値なので Swift 6 の検査にも素直に通ります。yield を await に置き換えただけの見た目なので、redux-saga を知っている人なら対応表を見ればすぐに読めますし、知らない人には普通の async 関数に見えるはずです。

ロックインを避ける構成も、実際に使ってみて安心感がありました。サンプルの Domain は swift-redux-saga を一度も import していない(import しているのは Foundation だけ)ので、このライブラリをやめる日が来ても、捨てるコードの範囲がはっきりしています。

ただ、どのアプリにも勧められるわけではありません。Action を enum で定義し、Reducer と Saga に分けて書くので、画面が 1 つで通信も 1 つというアプリでは、コードが増えるだけです。Redux と Saga の考え方を知らない人には、慣れるまでの学習コストもかかります。

Xcode 26 の既定の設定にも注意が要ります。Xcode 26 で作ったアプリのターゲットでは default MainActor isolation が有効になっていて、型や関数が暗黙にメインアクターに属します。Saga はメインアクターの外で動くので、State や Action、Saga には nonisolated を付けて宣言しなければなりません。付け忘れたときのエラーメッセージから原因を読み取りにくいので、README に例を載せました。State と Saga を UI とは別のターゲットに置けば、そのターゲットではこの設定を無効にでき、nonisolated を書かずに済みます。

TCA と比べると、swift-redux-saga は状態管理と副作用だけに絞った小さなライブラリです。画面遷移や依存性の注入は、アプリで好きな方法を選べます。逆に、そこまでまとめて面倒を見てほしいなら TCA の方が向いています。

旧記事でも書いたとおり、このアーキテクチャはニッチだと自認しています。それでも、Redux ベースのアーキテクチャに興味がある方や、時間の絡む処理の書き方に悩んでいる方には、試す価値があると思います。

まとめ

前回の記事では、ReSwift の上に Saga の考え方を取り入れるところまでで精一杯でした。今回は Swift Concurrency、Swift 6 の strict concurrency、Observation がそろったので、Redux 本体から作り直しました。put や takeLatest、エラー処理、テストまで用意して、ようやく redux-saga と呼べる形になりました。

はじめての方は、カウンターのアプリを作りながら順に説明するはじめかたから読んでみてください。ReSwift や ReSwift-Saga を使っている方向けには、移行ガイドも用意しています。感想や不具合の報告は、GitHub の Issue でお待ちしています。

Swift で書く Redux Saga に、iOS アプリ開発者の方が少しでも興味を持っていただけたら嬉しいです。

  1. ReSwift ↩

  2. ReSwift-Saga(旧ライブラリ。現在は非推奨) ↩

  3. swift-redux-saga ↩

  4. Redux ↩

  5. Redux Toolkit ↩

  6. Announcing Swift 6 ↩

  7. ReSwift-Thunk ↩

  8. The Composable Architecture ↩

  9. ReactorKit ↩

  10. redux-saga(GitHub) ↩

  11. Redux: Side Effects Approaches ↩

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?