TCAでdebounceを使う[小ネタ]
この記事は、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)
実装はとてもシンプルです。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