IOS 13での新しいCombineフレームワークの使用。
非常に不規則な速度で値を送信している上流パブリッシャーがいるとします。場合によっては、数秒または数分が値なしで通過し、その後、値のストリームが一度に送信されることがあります。上流の値をサブスクライブし、それらをバッファーに入れて、定期的な既知のケイデンスで送信するカスタムパブリッシャーを作成しますが、すべて使い尽くされた場合は何も発行しません。
具体的な例の場合:
アップストリームにサブスクライブした私のパブリッシャーは、1秒ごとに値を生成します:
Combineの既存のパブリッシャーまたはオペレーターは、ここでquiteしたいことを実行していないようです。
throttle
および debounce
は、特定のケイデンスで上流の値をサンプリングし、欠落している値をドロップします(たとえば、 "a "ケイデンスが1000msの場合)delay
は、すべての値に同じ遅延を追加しますが、間隔を空けません(たとえば、私の遅延が1000ミリ秒の場合、「a」を6001ミリ秒で、「b」を6002ミリ秒でパブリッシュします。 6003ミリ秒で「c」)buffer
は有望に思われますが、それを使用する方法、つまりオンデマンドでバッファから値を強制的に公開する方法を理解することはできません。シンクをbuffer
に接続すると、バッファリングはまったく行われず、すべての値が即座に公開されるように見えました。Zip
やmerge
やcombineLatest
のようなある種の結合演算子を使用して、それをTimerパブリッシャーと結合することを考えました、そしてそれはおそらく正しいアプローチですが、私はできません私が望む振る舞いをするようにそれを構成する方法を正確に理解してください。
編集
これは私が何をしようとしているのかをうまく説明している大理石の図です:
Upstream Publisher:
-A-B-C-------------------D-E-F--------|>
My Custom Operator:
-A----B----C-------------D----E----F--|>
編集2:単体テスト
modulatedPublisher
(私の希望するバッファリングされたパブリッシャー)が希望どおりに機能する場合に合格する必要がある単体テストを次に示します。完璧ではありませんが、受信したイベント(受信時刻を含む)を格納し、イベント間の時間間隔を比較して、目的の間隔より小さくないことを確認します。
func testCustomPublisher() {
let expectation = XCTestExpectation(description: "async")
var events = [Event]()
let passthroughSubject = PassthroughSubject<Int, Never>()
let cancellable = passthroughSubject
.modulatedPublisher(interval: 1.0)
.sink { value in
events.append(Event(value: value, date: Date()))
print("value received: \(value) at \(self.dateFormatter.string(from:Date()))")
}
// WHEN I send 3 events, wait 6 seconds, and send 3 more events
passthroughSubject.send(1)
passthroughSubject.send(2)
passthroughSubject.send(3)
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(6000)) {
passthroughSubject.send(4)
passthroughSubject.send(5)
passthroughSubject.send(6)
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(4000)) {
// THEN I expect the stored events to be no closer together in time than the interval of 1.0s
for i in 1 ..< events.count {
let interval = events[i].date.timeIntervalSince(events[i-1].date)
print("Interval: \(interval)")
// There's some small error in the interval but it should be about 1 second since I'm using a 1s modulated publisher.
XCTAssertTrue(interval > 0.99)
}
expectation.fulfill()
}
}
wait(for: [expectation], timeout: 15)
}
私が得た最も近いものは、次のようにZip
を使用しています。
public extension Publisher where Self.Failure == Never {
func modulatedPublisher(interval: TimeInterval) -> AnyPublisher<Output, Never> {
let timerBuffer = Timer
.publish(every: interval, on: .main, in: .common)
.autoconnect()
return timerBuffer
.Zip(self, { $1 }) // should emit one input element ($1) every timer tick
.eraseToAnyPublisher()
}
}
これは最初の3つのイベント(1、2、3)を適切に調整しますが、2番目の3つ(4、5、6)は調整しません。出力:
value received: 1 at 3:54:07.0007
value received: 2 at 3:54:08.0008
value received: 3 at 3:54:09.0009
value received: 4 at 3:54:12.0012
value received: 5 at 3:54:12.0012
value received: 6 at 3:54:12.0012
Zip
には内部バッファリング能力があるため、これは起こっていると思います。最初の3つのアップストリームイベントはバッファリングされ、タイマーのケイデンスで出力されますが、6秒の待機中にタイマーのイベントがバッファーされます。ペアリングされ、すぐに解雇されます。
たぶん......だろう Publishers.CollectByTime
ここのどこかで役に立ちますか?
Publishers.CollectByTime(upstream: upstreamPublisher.share(), strategy: Publishers.TimeGroupingStrategy.byTime(RunLoop.main, .seconds(1)), options: nil)
単一の壊れていないパイプラインを可能にするために、私が以前のRobの回答を調整してカスタムパブリッシャーに変換したことを述べたかっただけです(彼のソリューションの下のコメントを参照)。私の適応は以下ですが、すべての功績はまだ彼にあります。このカスタムパブリッシャーは内部でそれらを使用するため、Robのstep
演算子とSteppingSubscriber
も引き続き使用します。
編集:modulated
演算子の一部としてバッファーで更新されます。それ以外の場合は、アップストリームイベントをバッファーするためにアタッチする必要があります。
public extension Publisher {
func modulated<Context: Scheduler>(_ pace: Context.SchedulerTimeType.Stride, scheduler: Context) -> AnyPublisher<Output, Failure> {
let upstream = buffer(size: 1000, prefetch: .byRequest, whenFull: .dropNewest).eraseToAnyPublisher()
return PacePublisher<Context, AnyPublisher>(pace: pace, scheduler: scheduler, source: upstream).eraseToAnyPublisher()
}
}
final class PacePublisher<Context: Scheduler, Source: Publisher>: Publisher {
typealias Output = Source.Output
typealias Failure = Source.Failure
let subject: PassthroughSubject<Output, Failure>
let scheduler: Context
let pace: Context.SchedulerTimeType.Stride
lazy var internalSubscriber: SteppingSubscriber<Output, Failure> = SteppingSubscriber<Output, Failure>(stepper: stepper)
lazy var stepper: ((SteppingSubscriber<Output, Failure>.Event) -> ()) = {
switch $0 {
case .input(let input, let promise):
// Send the input from upstream now.
self.subject.send(input)
// Wait for the pace interval to elapse before requesting the
// next input from upstream.
self.scheduler.schedule(after: self.scheduler.now.advanced(by: self.pace)) {
promise(.more)
}
case .completion(let completion):
self.subject.send(completion: completion)
}
}
init(pace: Context.SchedulerTimeType.Stride, scheduler: Context, source: Source) {
self.scheduler = scheduler
self.pace = pace
self.subject = PassthroughSubject<Source.Output, Source.Failure>()
source.subscribe(internalSubscriber)
}
public func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
subject.subscribe(subscriber)
subject.send(subscription: PaceSubscription(subscriber: subscriber))
}
}
public class PaceSubscription<S: Subscriber>: Subscription {
private var subscriber: S?
init(subscriber: S) {
self.subscriber = subscriber
}
public func request(_ demand: Subscribers.Demand) {
}
public func cancel() {
subscriber = nil
}
}