🚥

TCAでdebounceを使う[小ネタ]

2022/12/18に公開約4,000字

この記事は、The Composable Architecture Advent Calendar 2022 12/19の記事です。

はじめに

昨今もっぱらSwift Concurrencyが話題ですが、Reactive programmingネタいかせていただきやす。
RxSwiftやCombineでおなじみのdebounceオペレーター。
TCAにも用意されているので、小ネタとして、実用例をご紹介しようと思います。

Debounce

まず、Debounceがどういった効果を持ったオペレーターなのかを改めて確認したいと思います。

Publishes elements only after a specified time interval elapses between events. debounce(for:scheduler:options:)

CombineのDebounceオペレーターは「イベントとイベントの間に指定された時間間隔が経過した後にのみ、要素をPublishする」と説明されています。イメージ図を書いてみました👇

上の図は、「何かのイベントが発生してから1分経過してから他のイベントが発生した場合はPublishする」ということで、逆に言えば、「何かのイベントが発生してから1分以内に他のイベントが発生した場合はPublishしない」ということです。

この仕組みを使って、今回TCAで
「1分間Actionが何も実行されなかったら、ResetActionを実行する」
ということを、実現してみようと思います。

実装

全体の実装はこんな感じになります。

public struct AppReducer: ReducerProtocol {
    public init() {}

    public struct State: Equatable {
        public init() {}
    }

    public enum Action: Equatable {
        case a
        case b
        case c
        case reset // ResetAction!!
    }
    
    @Dependency(\.mainQueue) private var mainQueue

    public var body: Reduce<State, Action> {
        Reduce { state, action in
            switch action {
            case .a:
                return .none
            case .b:
                return .none
            case .c:
                return .none
            case .reset:
                return .none
            }
        }
        Reduce { state, action in
            enum ActionDebounceID {}
            // Initialize if no action is taken for 1 minutes
            return .init(value: .reset)
                .debounce(
                    id: ActionDebounceID.self,
                    for: 60,
                    scheduler: mainQueue
                )
        }
    }
}

解説

bodyの中に2つのReduceがあるのがわかるかと思います。
なぜ2つのReduceを用意したのかというと、前者はそれぞれのActionに対する実装を行い、後者は全てのActionに対する実装、つまり、「1分間(いずれかの)Actionが何もSendされなかったら」という要件を実装をするために、用意しています。

では、後者のReduceの実装をみてみましょう。
まずEffectPublisher.init(value: .reset)を生成し、debounceオペレーターを使用してDebounce可能なEffectに変更しています。debounceオペレーターにはID、debounceさせたい時間、スケジューラーの3つを指定しています。そして、Actionが流れてくる度に、このEffectをReturnしています。

これで、「1分間Actionが何も実行されなかったら、ResetActionを実行する」が実現できました🎉

...といっても、とっても不思議ですよね。一体どういう仕組みで実現しているか、TCAのdebounceオペレーターの実装を一部見てみます。

      return Self(
        operation: .publisher(
          Just(())
            .setFailureType(to: Failure.self)
            .delay(for: dueTime, scheduler: scheduler, options: options)
            .flatMap { self.publisher.receive(on: scheduler) }
            .eraseToAnyPublisher()
        )
      )
      .cancellable(id: id, cancelInFlight: true)

Debouncing

実装はとてもシンプルです。CombineのJust.delayを使って、「○時間後にEffectを実行する」というのを実現しています。では「○時間の間、Effectが何も実行されなかったら」はどうやってハンドリングしているのでしょう?
.cancellable(id: id, cancelInFlight: true)cancelInFlightに注目です👀

Determines if any in-flight effect with the same identifier should be canceled before starting this new one.

cancelInFlightパラメーターにtrueを渡すことによって、もしも同じIDのEffectがある場合、古い方はキャンセルされるようになります。

先ほど、 以下の説明をしたかと思います。

Actionが流れてくる度に、このEffectをReturnしています。

Returnしているdebounceは常に同じIDを使っているので、.delayで待機している間に同じIDが流れてきたら、古い方はキャンセルされます。この仕組みをつかって、「1分間Actionが何も実行されなかったら、ResetActionを実行する」を実現しているというわけです💡

まとめ

いかがでしたでしょうか、TCAのdebounce。便利ですよね、面白いですよね😄
今回は久々に、Reactive programming関連のお話でしたが、
TCAのライブラリを覗いてみると、Swift Concurrencyでdebounceを行う方法として、 withTaskCancellationを利用するという手段が紹介されていました。
時代の移り変わり。
好きでしたReactive programming。
これから仲良くしてねSwift Concurrency。

今年のアドベントカレンダーはこれで終わり!
これから、W杯決勝みるぞい⚽️
では皆様、良いお年を🍺

完。

W杯決勝後:
ありがとう、フランス!おめでとう、アルゼンチン!
凄まじく、最高の試合で感動した!!

Discussion

ログインするとコメントできます