Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Redux の副作用を直感的に管理する Redux Saga を Swift でも使いたい

Redux の副作用を直感的に管理する Redux Saga を Swift でも使いたい

Redux の副作用を直感的に管理する Redux Saga を Swift でも使いたい
iOSDC Japan 2023 #iosdc - fortee.jp
https://fortee.jp/iosdc-japan-2023/proposal/d3c4feb1-0b2c-403b-b47c-6db885bc70d8

download PDF
https://github.com/mitsuharu/iosdc-2023-pamphlet/releases/tag/2023-07-05

Mitsuharu Emoto

September 07, 2023
Tweet

More Decks by Mitsuharu Emoto

Other Decks in Programming

Transcript

  1. Redux
    の副作用を直感的に管理する
    Redux Saga
    を Swift
    でも使いたい
    江本光晴(株式会社ゆめみ)
    @mitsuharu_e / @mitsuharu.bsky.social
    あなたのお気に入りのアーキテクチャは何ですか。私のお気に入りは Redux Saga
    です。
    Redux Saga 1
    は単方向データフローの Redux 2
    を拡張し、非同期処理や副作用を直感
    的に管理できるようにしたアーキテクチャです。ビジネスロジックなどを Saga
    にまとめ
    ることで、責務を明確に分けることができます。
    Redux Saga
    は JavaScript
    で作成され Web
    (React
    )や React Native
    などの開発でよ
    く用いられています。同じ宣言的 UI
    の SwiftUI
    との相性が期待できます。しかし、残念
    なことに Swift
    で Redux Saga
    を実装したライブラリはありません。
    それならば、自身で作成するしかありません。JavaScript
    と Swift
    の言語設計と性質の
    違いを考慮しつつ、Swift
    の言語特性を活かす形で、Redux Saga
    の主要な機能をどのよ
    うに実装するかを解説します。Redux Saga
    の特性や利点を紹介して、iOS
    アプリ開発に
    おける Redux Saga
    の可能性を探求します。
    本記事では、Swift
    だけでなく JavaScript
    (TypeScript
    )のソースコードも提示します。
    また、Redux Saga
    の API
    も挙げますが、詳細説明は省略します。雰囲気を感じてもら
    う程度で問題ありません。
    Redux Saga
    とは
    Redux
    は、JavaScript
    アプリの状態管理のための予測可能な状態コンテナです。アプリ
    全体の状態を一元的に管理ができて、データフローを単純化して管理を容易にします。し
    かし、Redux
    は非同期処理や副作用(データフェッチングやデータベースへのアクセスな
    ど)の管理が設計されていないため、それらの実装方法は明確に定められていません。こ
    れは Redux
    の主な弱点の1つとされています。
    そこで Redux Saga
    の登場です。Redux Saga
    は、非同期処理や副作用を直感的に管理
    するライブラリです。Saga
    はアプリの中で副作用を個別に実行する独立したスレッドの
    ような動作イメージです。Redux Saga
    は middleware
    として設計されているため、
    Saga
    は Action
    に応じて起動、一時停止、中断ができます。State
    全体にアクセスでき、
    Action
    の発行もできます。
    同様なライブラリの Redux Thunk
    と比較すると、コールバック地獄に陥ることなく、非
    同期フローを簡単にテスト可能にし、Action
    を純粋に保つことができます。

    View Slide

  2. Redux Saga
    のデータフロー
    たとえば、あるボタンをタップして、ユーザー情報を取得する例を考えましょう。この場
    合、タップイベントで「ユーザー情報を取得する」という Action
    を発行します。
    // View などでユーザー情報を取得する Action を発行(dispatch)する
    const onPress = () => {
    dispatch(requestUser({userId: '1234'}))
    }
    事前に Redux Saga
    側で Action
    と Saga
    を関連付けておきます。takeEvery
    は特定の
    Action
    が発行されるのを待ち、それが発行されたら Saga
    を実行します。onPress()

    requestUser
    が発行されたので、関連付けられた requestUserSaga
    が実行されます。
    // Redux Saga の初期設定時に Action に対応する処理を設定しておく
    function* rootSaga() {
    // Action "requestUser" が発行されたら、requestUserSaga を実行する
    yield takeEvery(requestUser, requestUserSaga)
    }
    // ユーザー情報の取得を行う副作用
    function* requestUserSaga(action) {
    // たとえば、API からユーザー情報を取得する
    }
    副作用は Saga
    にまとめて、View
    は必要な Action
    を発行するだけです。Redux Saga
    にしたがっていれば、自然と責務分けが実現されます。私が Redux Saga
    の好きな特徴
    の1つです。

    View Slide

  3. Swift
    での実装アプローチ
    Redux Saga
    の実装や機能は複雑なため、完全再現は目指しません。一部機能の再現およ
    び実装を目標とします。今回は、middleware, call, take
    そして takeEvery
    を対象とし
    ます。middleware
    は Redux
    から Redux Saga
    へ Action
    を伝える根底部分で、call,
    take, takeEvery
    はよく利用される機能です。
    元々の JavaScript
    の実装は Saga
    にジェネレーター関数を用いていますが、Swift
    では
    Swift Concurrency
    を利用します。Action
    の発行や監視の非同期制御は Combine
    で行
    います。なお、Redux
    本体の実装は既存ライブラリの ReSwift 3
    を利用します。
    ここで、Redux
    本体との接点を最小限に抑え、独立性の高いライブラリを目指します。
    Saga
    としてビジネスロジックを切り離して管理できるので、たとえば新たに優れたアー
    キテクチャが登場した場合でも、そのアーキテクチャへの入替を容易にするためです。
    今回は Xcode 14.3.1
    で開発しています。現在も開発中なため、紹介するソースコードは
    変更される場合があります。ご了承ください。
    Swift
    で実装する
    まず Redux Saga
    の実装において Action
    の比較が必要になります。ここでいう比較はイ
    ンスタンス同士の比較ではなく、Action
    の種類、つまり型での比較です。ReSwift
    が定義
    する Action
    は空の Protocol
    で、一般に enum
    や struct
    で利用されることが多いです。
    enum
    は型の比較が難しい、struct
    は実装の過程で継承を利用したいので難しいです(継
    承を利用する主な目的は reducer
    の設計で、その詳細は省略します)
    。そのため、今回は
    class
    で Action
    を定義します。
    class SagaAction: Action {}
    先ほど挙げた例と同様に、ユーザー情報を取得する場合を考えます。
    // Action をグループ管理したいので UserAction という中間のクラスを作る
    class UserAction: SagaAction {}
    // ユーザー情報を取得する Action
    final class RequestUser: UserAction {
    let userID: String
    init(userID: String) {
    self.userID = userID
    }
    }

    View Slide

  4. middleware
    を実装する
    Redux
    で発行された Action
    を Redux Saga
    に伝達させる middleware
    を実装します。
    まず Action
    を Redux Saga
    向けに発行するクラスを実装します。クラス名は Channel
    にしました。このクラスが自作する Redux Saga
    の中核になります。
    final class Channel {
    public static let shared = Channel()
    private let subject = PassthroughSubject()
    // action を発行する
    func put(_ action: SagaAction){
    subject.send(action)
    }
    }
    この Channel
    を組み込んだ middleware
    を実装します。
    func createSagaMiddleware() -> Middleware {
    return { dispatch, getState in
    return { next in
    return { action in
    if let action = action as? SagaAction {
    Channel.shared.put(action)
    }
    return next(action)
    }
    }
    }
    }
    この middleware
    を ReSwift
    の Store
    に適用します。Redux
    のデータフローに介入し
    て、発行された Action
    を Redux Saga
    に伝達させます。
    // ReSwift の初期設定を行う関数
    func makeAppStore() -> Store {
    // Saga 用の middleware を作成する
    let sagaMiddleware: Middleware = createSagaMiddleware()
    let store = Store(
    reducer: reducer,
    state: State.initialState(),
    middleware: [sagaMiddleware]
    )
    return store
    }

    View Slide

  5. call
    を実装する
    call
    は Saga
    の関数と引数を与えて実行するシンプルな関数です。ここで Saga
    関数の型
    を定義します。Action
    を引数にした非同期関数です。
    typealias Saga = (SagaAction) async -> T
    この型を使って call
    を次のように実装しました。Saga
    の型定義でジェネリクスを利用し
    ましたが、開発中のため Any
    にしました。今後の修正課題です。
    @discardableResult
    func call(_ effect: @escaping Saga,
    _ arg: SagaAction) async -> Any {
    return await effect(arg)
    }
    take
    を実装する
    take
    は特定の Action
    が発行されるのを待ちます。注意する点として Action
    のインスタ
    ンスを比較するのではなく、発行された Action
    の種類(型)を判定します。まずは前述
    の Channel
    に、特定の Action
    の型を受信する仕組みを実装します。
    final class Channel {
    // ...
    // deinit などで忘れずに解放する(省略)
    private var subscriptions = [AnyCancellable]()
    // 引数で指定した action の型が発行されるまで待つ
    func take(_ actionType: SagaAction.Type ) -> Future
    {
    return Future { [weak self] promise in
    guard let self = self else {
    return
    }
    self.subject.filter {
    type(of: $0) == actionType
    }.sink { _ in
    // 必要に応じてエラー処理を行う
    } receiveValue: {
    promise(.success($0))
    }.store(in: &self.subscriptions)
    }
    }
    }

    View Slide

  6. 追加改修した Channel
    を利用して take
    の関数を実装します。この take()
    を実行する
    と、引数で指定した Action
    の型の監視が始まり、検出されるまで待ちます。
    @discardableResult
    func take(_ actionType: SagaAction.Type) async -> SagaAction {
    let action = await Channel.shared.take(actionType).value
    return action
    }
    この take()
    は Redux Saga
    の起点となる機能の1つです。Action
    の種類、つまり型で
    判断するという処理の設計や実装は、納得するまで何度も作り直しました。今回の実装で
    苦労した点です。
    takeEvery
    を実装する
    takeEvery
    は特定の Action
    と Saga
    を関連付けて、その Action
    が発行されるたびに指
    定した Saga
    を実行します。前述で作成した take
    と call
    を組み合わせて実装します。
    func takeEvery( _ actionType: SagaAction.Type,
    saga: @escaping Saga) {
    Task.detached {
    while true {
    let action = await take(actionType)
    await call(saga, action)
    }
    }
    }
    無限ループ!?という感覚は正常です。ループの中で Action
    が発行されるまで待ち、それ
    が発行されたら Saga
    を実行するという処理を繰り返します。
    自作した Redux Saga
    を使おう
    一連の実装が終わりました。takeEvery
    を使った簡単な例を紹介します。まずは、実行さ
    せたい処理を Saga
    関数で実装します。オリジナルの実装では Saga
    関数を慣習的に
    xxxSaga
    と命名することが多いです。Swift
    でも、その慣習にそって、命名しました。
    // ユーザー情報を取得する Saga
    let requestUserSaga: Saga = { action async in
    guard let action = action as? RequestUser else { return }
    // API などで action.userID のユーザー情報を取得する
    }

    View Slide

  7. 次に takeEvery
    で Action
    と Saga
    を関連付けます。
    // Saga を設定する関数
    func setupSaga(){
    takeEvery(RequestUser.self, saga: requestUserSaga)
    }
    これは前述の makeAppStore()
    で middleware
    を設定した後に呼ぶとよいです。
    func makeAppStore() -> Store {
    // ...
    // store, middleware の設定後に呼ぶ
    setupSaga()
    return store
    }
    準備が整いました。適当な View
    向けの関数で Action RequestUser
    を発行する処理を作
    成します。今回は MVVM
    を想定して、適当な ViewModel
    を用意しました。
    final class UserViewModel {
    // 適当なボタンイベントなどで呼ぶ
    func requestUser() {
    store.dispatch(RequestUser(userID: "1234"))
    }
    }
    Redux
    から発行された Action RequestUser
    は middleware
    を通じて Redux Saga

    伝達されます。そして takeEvery
    により Saga requestUserSaga
    が実行されます。View
    は Action
    を発行するだけで、実行される処理の責務には関与しません。
    評価と考察
    Redux Saga
    の主な機能を再現して、アプリの副作用を Saga
    にまとめることができまし
    た。View
    での処理がとてもシンプルになり満足しています。しかし、まだ対応・修正し
    たいところも残っています。まだまだ開発途中です。
    残りの未実装な機能を実装する
    Action
    を enum, struct
    でも利用できるようにしたい
    Saga
    のジェネリクスを適切に対応して、より型安全にする
    エラー処理やテストコードなどを適切に整備して、安全にする

    View Slide

  8. Redux Saga
    と SwiftUI
    SwiftUI
    を利用した開発では Redux
    ベースのアーキテクチャとの相性がよいといわれて
    います。しかしながら、SwiftUI
    の実装や癖などから Apple Platform
    においては、私は
    必ずしもベストマッチだとは言い切れないとも考えています。
    私が iOS
    アプリを個人開発する場合、Redux
    (ReSwift
    )+ MVVM
    でアプリ設計をする
    ことが多いです。Apple Platform
    では MVVM
    の選択が無難だが、Redux
    の利点も捨て
    きれないためです。状態は Redux
    で管理して、副作用などは ViewModel
    で定義してい
    ます。今回自作した Redux Saga
    により、副作用も Redux
    側で管理できるようになりま
    した。ViewModel
    は Action
    の発行と状態を View
    へ渡すだけのシンプルな構造になり、
    MVVM
    でしばしば問題にされる Fat ViewModel
    は解消されました。
    しかし、このアーキテクチャはニッチだと自認しています。全員には勧めません。Redux
    Saga
    の学習コストは比較的高いとされています。Redux
    ベースのアーキテクチャに興味
    ある方、プロジェクトの構造を大きく変えずにまずは試したい方、いかがでしょうか。
    まとめ
    本記事は、JavaScript
    ベースのライブラリ Redux Saga
    を Swift
    で実装する方法につい
    て解説しました。JavaScript
    と Swift
    は言語の設計と性質が異なるため、Redux Saga
    の完全な再現は難しいです。実際に多くの試作して上手くいかないこともあり、ChatGPT
    にも相談しました。完全再現は諦めて、その概念を取り入れ、Swift
    の特性を活かす形で
    実装を試みて、ようやく形になりました。
    今回は middleware, call, take
    そして takeEvery
    の実装を紹介しました。紙面の都合上
    から取り上げなかった他の機能 put, fork, selector, takeLeading
    や takeLatest
    なども
    実装しています。それらの実装を含め、ソースコードは GitHub
    で公開しています。
    https://github.com/mitsuharu/ReSwiftSagaSample
    現段階は開発・検証のためのサンプルコードですが、将来的には OSS
    としてリリースし
    たいと考えています。Redux
    をベースとしたアーキテクチャのライブラリ、たとえば
    ReSwift
    や TCA
    などは、すでに多くのアプリで利用されています。今回紹介した Redux
    Saga
    も iOS
    アプリ開発者に興味を持って頂けたら嬉しいです。
    1. https://github.com/redux-saga/redux-saga↩
    2. https://github.com/reduxjs/redux↩
    3. https://github.com/ReSwift/ReSwift
    バージョン 6.1.1
    を利用しました↩

    View Slide