Does Swift's Combine framework have a sample(on:) operator similar to those in RXSwift or Reactive Swift? - rx-swift

Does anyone know how to recreate sampling behavior in Combine?
Here's a diagram of the sample's behavior in RXMarbles
The gist of sample() is that there are two streams, when one is triggered, the latest value of the other stream is sent if it already hasn't been sent.

The CombineExt library has the withLatestFrom operator which does what you want, along with many other useful operators.

Here is a Playground that might do what you want. I didn't do a whole lot of testing on it so please proceed with caution:
import UIKit
import Combine
import PlaygroundSupport
struct SamplePublisher<DataSeq, Trigger, E> : Publisher
where DataSeq : Publisher,
Trigger : Publisher,
DataSeq.Failure == Trigger.Failure,
E == DataSeq.Failure,
DataSeq.Output : Equatable {
typealias Output = DataSeq.Output
typealias Failure = E
// The two sequences we are observing, the data sequence and the
// trigger sequence. When the trigger fires it will send the
// latest value from the dataSequence UNLESS it hasn't changed
let dataPublisher : DataSeq
let triggerPublisher : Trigger
struct SamplePublisherSubscription : Subscription {
var combineIdentifier = CombineIdentifier()
let dataSubscription : AnyCancellable
let triggerSubscription : Subscription
func request(_ demand: Subscribers.Demand) {
triggerSubscription.request(demand)
}
func cancel() {
dataSubscription.cancel()
triggerSubscription.cancel()
}
}
func receive<S>(subscriber: S) where S : Subscriber, E == S.Failure, DataSeq.Output == S.Input {
var latestData : DataSeq.Output?
var lastSent : DataSeq.Output?
var triggerSubscription : Subscription?
// Compares the latest value sent to the last one that was sent.
// If they don't match then it sends the latest value along.
// IF they do match, or if no value has been sent on the data stream yet
// Don't emit a new value.
func emitIfNeeded() -> Subscribers.Demand {
guard let latest = latestData else { return .unlimited }
if nil == lastSent ||
lastSent! != latest {
lastSent = latest
return subscriber.receive(latest)
} else {
return .unlimited
}
}
// Here we watch the data stream for new values and simply
// record them. If the data stream ends, or erors we
// pass that on to our subscriber.
let dataSubscription = dataPublisher.sink(
receiveCompletion: {
switch $0 {
case .finished:
subscriber.receive(completion: .finished)
case .failure(let error):
subscriber.receive(completion: .failure(error))
}
},
receiveValue: {
latestData = $0
})
// The thing that subscribes to the trigger sequence.
// When it receives a value, we emit the latest value from the data stream (if any).
// If the trigger stream ends or errors, that will also end or error this publisher.
let triggerSubscriber = AnySubscriber<Trigger.Output,Trigger.Failure>(
receiveSubscription: { subscription in triggerSubscription = subscription },
receiveValue: { _ in emitIfNeeded() },
receiveCompletion: {
switch $0 {
case .finished :
emitIfNeeded()
subscriber.receive(completion: .finished)
case .failure(let error) :
subscriber.receive(completion: .failure(error))
}
})
// subscribe to the trigger sequence
triggerPublisher.subscribe(triggerSubscriber)
// Record relevant information and return the subscription to the subscriber.
subscriber.receive(subscription: SamplePublisherSubscription(
dataSubscription: dataSubscription,
triggerSubscription: triggerSubscription!))
}
}
extension Publisher {
// A utility function that lets you create a stream that is triggered by
// a value being emitted from another stream
func sample<Trigger, E>(trigger: Trigger) -> SamplePublisher<Self, Trigger, E>
where Trigger : Publisher,
Self.Failure == Trigger.Failure,
E == Self.Failure,
Self.Output : Equatable {
return SamplePublisher( dataPublisher : self, triggerPublisher : trigger)
}
}
var count = 0
let timer = Timer.publish(every: 5.0, on: RunLoop.current, in: .common).autoconnect().eraseToAnyPublisher()
let data = Timer.publish(every: 1.0, on: RunLoop.current, in: .common)
.autoconnect()
.scan(0) { total, _ in total + 1}
var subscriptions = Set<AnyCancellable>()
data.sample(trigger: timer).print()
.sink(receiveCompletion: {
debugPrint($0)
}, receiveValue: {
debugPrint($0)
}).store(in: &subscriptions)
PlaygroundSupport.PlaygroundPage.current.needsIndefiniteExecution = true

Related

Handling dependencies between subjects in a manageable way

I develop a project which utilizes a dynamic data structure (tree-like). Both number of nodes in the structure change over time as well as data that is kept in the nodes. Some properties affect other - both within a certain node but also inside parent/children.
I've managed to get to the point where changes propagate correctly but a problem arose. The piece of code which sets up subscriptions and contains logic of propagation is a complete mess - due to the fact that I nest subscriptions set up. I'm new to the Combine framework so probably I don't know how to use it correctly. I'm hoping to get a suggestion.
Story that hopefully illustrate the problem
Imagine that you have a tree and if you subscribe to a node's data
stream you are going to receive data from the node itself as well as
from its ancestors. The problem is that in order to get a data from
one subject you must go through a different one.
Code ready for copying and pasting to the playground:
//
// Copyright © 2021 Mateusz Stompór. All rights reserved.
//
import Combine
// Helper
struct WeakRef<T: AnyObject> {
weak var reference: T?
init(_ reference: T?) {
self.reference = reference
}
}
// Parent is observable in order to react for change
class Node<T> {
let data: T
var parent: CurrentValueSubject<WeakRef<Node>, Error>
let stream: CurrentValueSubject<T, Error>
private var parentSubscription: AnyCancellable?
private var parentStreamSubscription: AnyCancellable?
init(data: T, parent: Node?) {
self.data = data
self.parent = CurrentValueSubject(WeakRef(parent))
self.stream = CurrentValueSubject(data)
setup()
}
func setup() {
parentSubscription = parent.compactMap({ $0.reference?.stream }).sink(receiveCompletion: { [weak self] _ in
self?.parentStreamSubscription?.cancel()
}, receiveValue: { [weak self] stream in
self?.parentStreamSubscription = stream.sink { _ in
// Nothing needed
} receiveValue: { value in
guard let self = self else {
return
}
self.stream.send(value)
}
})
}
}
let parent = Node(data: 2, parent: nil)
let child = Node(data: 1, parent: nil)
let subscription = child.stream.sink { _ in
// nothing needed
} receiveValue: { value in
print(value)
}
// '1' is printed right away
// Setup connection
child.parent.send(WeakRef(parent))
// '2' is printed once connection is set
parent.stream.send(3)
// '3' is printed
// Changing child's parent
let newParent = Node(data: 4, parent: nil)
child.parent.send(WeakRef(newParent))
// '4' is printed as parent change
parent.stream.send(5)
// '5' is NOT printed, node is no longer part of the tree
newParent.stream.send(6)
// '6' is printed
The core question: is there a way to avoid this kind of nesting?

RxSwift: Calling onCompleted after onNext delivers only the completed event

I'm wrapping some legacy completion-block code in an Observable. It will emit one event (next or error), and then complete. The problem is that calling onNext(), onCompleted() only sends the completed event to the observer. Why doesn't the next event get delivered?
UPDATE: The people stream actually works as expected. The issue turns out to be in the next stream, filteredPeople. The inner completed event is passed along to it, and I'm just returning it, which terminates the stream.
I need to filter out completed events from inner streams.
let people = Observable<Event<[Person]>>()
.flatMapLatest {
return fetchPeople().asObservable().materialize()
}
.share()
// this is bound to a search field
let filterText = PublishSubject<String>()
let filteredPeople = Observable.combineLatest(people, filterText) { peopleEvent, filter in
// this is the problem. the completed event from people is being returned, and it terminates the stream
guard let people = peopleEvent.element else { return peopleEvent }
if filterText.isEmpty { return .next(people) }
return .next(people.filter { ... })
}
func fetchPeople() -> Single<[Person]> {
return Single<[Person]>.create { observer in
PeopleService.fetch { result in
switch result {
case .success(let people):
observer(.success(people))
case .failure(let error):
observer(.error(error))
}
}
return Disposables.create()
}
}
filteredPeople.subscribe(
onNext: { event in
// ?! doesn't get called
},
onCompleted: {
// we get here, but why?
},
onError: {event in
...
}).disposed(by: disposeBag)
You haven't posted the code that is causing the problem. The code below works as expected:
struct Person { }
class PeopleService {
static func fetch(_ result: #escaping (Result<[Person], Error>) -> Void) {
result(.success([]))
}
}
let disposeBag = DisposeBag()
func fetchPeople() -> Single<[Person]> {
return Single<[Person]>.create { observer in
PeopleService.fetch { result in
switch result {
case .success(let people):
observer(.success(people))
case .failure(let error):
observer(.error(error))
}
}
return Disposables.create()
}
}
let people = Observable<Void>.just(())
.flatMapLatest { _ in
return fetchPeople().asObservable().materialize()
}
.share()
people.subscribe(
onNext: { event in
print("onNext does get called")
print("in fact, it will get called twice, once with a .next(.next([Person])) event")
print("and once with a .next(.completed) event.")
},
onCompleted: {
print("this prints after onNext gets called")
})
.disposed(by: disposeBag)
I fixed it by filtering out completed events from the inner stream. I am not sure this is the right way, but I can't think of a better solution.
let people = Observable<Event<[Person]>>()
.flatMapLatest {
return fetchPeople()
.asObservable()
.materialize()
// Our work is done, but don't end the parent stream
.filter { !$0.isCompleted }
}
.share()

RxBluetoothKit - implement read and write protocol and automatically disconnect

I'm implementing a BLE protocol between a central (iPhone) and peripheral (custom device). The protocol works as follows:
central connects to peripheral and sets up notification
peripheral sends data on notification characteristic
central processes data and sends response on separate characteristic
peripheral sends addtnl data on notification characteristic
central process data and disconnects.
I'm attempting to implement this in a clean way using RxBluetoothKit. It currently works, but I'd like to solve the following challenges:
What is the best way to cleanly disconnect in step 5. I'm hoping to not have to dispose the overall observable, but rather just have it 'complete'. I'm currently using 'takeUntil', but not sure if that's the best way.
Allow for the notification to cleanup gracefully prior to disconnect. With my current code, I receive an 'API MISUSE can only accept commands while in the connected state' because I believe the notification is cleaning up while the disconnect is occurring.
Thanks.
enum TestPeripheralService: String, ServiceIdentifier {
case main = "CED916FA-6692-4A12-87D5-6F2764762B23"
var uuid: CBUUID { return CBUUID(string: self.rawValue) }
}
enum TestPeripheralCharacteristic: String, CharacteristicIdentifier {
case writer = "CED927B4-6692-4A12-87D5-6F2764762B2A"
case reader = "CED9D5D8-6692-4A12-87D5-6F2764762B2A"
var uuid: CBUUID { return CBUUID(string: self.rawValue) }
var service: ServiceIdentifier { return TestPeripheralService.main }
}
fileprivate lazy var centralManager: CentralManager = {
RxBluetoothKitLog.setLogLevel(.verbose)
return CentralManager(queue: .main)
}()
func executeConnectionAndHandshake() {
let disconnectSubject = PublishSubject<Bool>.init()
var peripheral: Peripheral?
var packetNum = 0
_ = centralManager
.observeState()
.startWith(centralManager.state)
.filter { $0 == .poweredOn }
.flatMap { _ in self.centralManager.scanForPeripherals(withServices: [TestPeripheralService.main.uuid]) }
.flatMap { $0.peripheral.establishConnection().takeUntil(disconnectSubject) }
.do(onNext: { peripheral = $0 })
.flatMap { $0.discoverServices([TestPeripheralService.main.uuid])}
.flatMap { $0[0].discoverCharacteristics(nil)}
.flatMap { _ in
Observable<Bool>.create { event in
let disposables = CompositeDisposable()
let readSubject = PublishSubject<Data>.init()
_ = disposables.insert(peripheral!.observeValueUpdateAndSetNotification(for: TestPeripheralCharacteristic.reader)
.subscribe(onNext: {
packetNum += 1
let packet = $0.value!
if (packetNum <= 1) {
readSubject.onNext(packet)
} else {
event.onNext(true)
event.onCompleted()
}
}, onError: { event.onError($0) })
)
_ = disposables.insert(readSubject
.flatMapLatest { data -> Single<Characteristic> in
var writeData = Data(capacity: 300)
for _ in 0..<300 {
writeData.append(0xFF)
}
return peripheral!.writeValue(writeData, for: TestPeripheralCharacteristic.writer, type: .withResponse)
}
.subscribe(onError: { event.onError($0) })
)
return Disposables.create {
disposables.dispose()
}
}
.do(onCompleted: { disconnectSubject.onNext(true) })
}
.subscribe(onError: { print($0) },
onCompleted: { print("Connection and handshake completed") })
}

Mapping of each emit -- SwitchMap guaranteeing atleast 1 emit / ConcatMap hybrid?

im breaking my mind around how to do this in RX.
The actual usecase is mapping of LowerLevelEvent(val userId: String) to HigherLevelEvent(val user: User), where the User is provided by observable, so it can emit n times, so example output
LowerLevelEvent1(abc) -> HigherLevelEvent1(userAbc(nameVariation1)
LowerLevelEvent2(abc) -> HigherLevelEvent2(userAbc(nameVariation1)
LowerLevelEvent3(abc) -> HigherLevelEvent3(userAbc(nameVariation1)
LowerLevelEvent4(abc) -> HigherLevelEvent4(userAbc(nameVariation1)
HigherLevelEvent4(userAbc(nameVariation2)
HigherLevelEvent4(userAbc(nameVariation3)
So my naive solution was to use combineLatest. So while userId is not changed user observable is subscribed, i.e. not resubscribed when new lowerLevelEmits & its userId is not changed
val _lowerLevelEventObservable: Observable<LowerLevelEvent> = lowerLevelEventObservable
.replayingShare()
val _higherLevelEventObservable: Observable<HigherLevelEvent> = Observables
.combineLatest(
_lowerLevelEventObservable,
_lowerLevelEventObservable
.map { it.userId }
.distinctUntilChanged()
.switchMap { userRepository.findByIdObservable(it)
) { lowerLevelEvent, user -> createHigherLevelInstance... }
However this has glitch issues, since both sources in combineLatest originate from same observable.
Then I thought about
lowerLevelObservable.
.switchMap { lowerLevelEvent ->
userRepository.findByIdObservable(lowerLevelEvent.userId)
.map { user -> createHigherLevelInstance... }
}
This however can break if lowerLevelObservable emits fast, and since user observable can take some time, given lowerLevelX event can be skipped, which I cannot have. Also it resubscribes user observable each emit, which is wasteful since it wont change most likely
So, maybe concatMap? That has issue of that the user observable doesnt complete, so concatMap wouldnt work.
Anyone have a clue?
Thanks a lot
// Clarification:
basically its mapping of A variants (A1, A2..) to A' variants (A1', A2'..) while attaching a queried object to it, where the query is observable so it might reemit after the mapping was made, so AX' needs to be reemited with new query result. But the query is cold and doesnt complete
So example A1(1) -> A1'(user1), A2(1) -> A2'(user1), A3(1) -> A3'(user1) -- now somebody changes user1 somewhere else in the app, so next emit is A3'(user1')
Based on the comments you have made, the below would work in RxSwift. I have no idea how to translate it to RxJava. Honestly though, I think there is a fundamental misuse of Rx here. Good luck.
How it works: If it's allowed to subscribe it will, otherwise it will add the event to a buffer for later use. It is allowed to subscribe if it currently isn't subscribed to an inner event, or if the inner Observable it's currently subscribed to has emitted an element.
WARNING: It doesn't handle completions properly as it stands. I'll leave that to you as an exercise.
func example(lowerLevelEventObservable: Observable<LowerLevelEvent>, userRepository: UserRepository) {
let higherLevelEventObservable = lowerLevelEventObservable
.flatMapAtLeastOnce { event in // RxSwift's switchLatest I think.
Observable.combineLatest(
Observable.just(event),
userRepository.findByIdObservable(event.userId),
resultSelector: { (lowLevelEvent: $0, user: $1) }
)
}
.map { createHigherLevelInstance($0.lowLevelEvent, $0.user) }
// use higherLevelEventObservable
}
extension ObservableType {
func flatMapAtLeastOnce<U>(from fn: #escaping (E) -> Observable<U>) -> Observable<U> {
return Observable.create { observer in
let disposables = CompositeDisposable()
var nexts: [E] = []
var disposeKey: CompositeDisposable.DisposeKey?
var isAllowedToSubscribe = true
let lock = NSRecursiveLock()
func nextSubscription() {
isAllowedToSubscribe = true
if !nexts.isEmpty {
let e = nexts[0]
nexts.remove(at: 0)
subscribeToInner(e)
}
}
func subscribeToInner(_ element: E) {
isAllowedToSubscribe = false
if let key = disposeKey {
disposables.remove(for: key)
}
let disposable = fn(element).subscribe { innerEvent in
lock.lock(); defer { lock.unlock() }
switch innerEvent {
case .next:
observer.on(innerEvent)
nextSubscription()
case .error:
observer.on(innerEvent)
case .completed:
nextSubscription()
}
}
disposeKey = disposables.insert(disposable)
}
let disposable = self.subscribe { event in
lock.lock(); defer { lock.unlock() }
switch event {
case let .next(element):
if isAllowedToSubscribe == true {
subscribeToInner(element)
}
else {
nexts.append(element)
}
case let .error(error):
observer.onError(error)
case .completed:
observer.onCompleted()
}
}
_ = disposables.insert(disposable)
return disposables
}
}
}

Reconnect to source and continue from last emitted value

I'm trying to figure out how to implement reconnect to observable after a transient failure, to continue from a last emitted value.
Assume I have the following method:
interface MyQuery {
fromId: number;
toId: number;
}
interface MyItem {
id: number;
val: string;
}
function observeUnstable(query: MyQuery): Observable<MyItem>;
The method observableUnstable lets to subscribe to a stream which emits values and may emit the following error in case of intermittent connection failure:
class DisconnectedError extends Error;
I want to compose a new observable which would wrap the original observable above and have transparent resubscribe from the position at which the previous subscription has failed.
The data types are going to be opaque, so I would want to make the reconnection logic generic, probably as an operator which would accept a high order selector function:
let startQuery = { fromId: 1, toId: 10 };
let reconnectable = observeUnstable(startQuery)
.lift(new ReconnectOperator<MyItem>((err, lastValue?) => {
if (err instanceof DisconnectedError) {
// This error indicates that we've been disconnected,
// resubscribing from the place we have stopped
let continueQuery = {
fromId: lastValue ? lastValue.id + 1 : startQuery.fromId,
toId: startQuery.toId
};
return observeUnstable(continueQuery);
} else {
// Rethrowing error we don't expect
throw err;
}
}));
Here are my ReconnectOperator and ReconnectSubscriber:
class ReconnectOperator<T> implements Operator<T, T> {
constructor(private handler: (err: any, lastValue?: T) => Observable<T>) {
}
call(subscriber: Subscriber<T>, source: any): any {
return source.subscribe(new ReconnectSubscriber(subscriber, this.handler));
}
}
class ReconnectSubscriber<T> extends Subscriber<T> {
private lastValue?: T;
constructor(destination: Subscriber<T>, private handler: (err: any, lastValue?: T) => Observable<T>) {
super(destination);
}
protected _next(value: T) {
this.lastValue = value;
super._next(value);
}
error(err: any) {
if (!this.isStopped) {
let result: Observable<T>;
try {
result = this.handler(err, this.lastValue);
} catch (err2) {
super.error(err2);
return;
}
// TODO: ???
result.subscribe(this._unsubscribeAndRecycle());
// this._unsubscribeAndRecycle();
//this.source.subscribe(result);
//this.add(subscribeToResult(this, result));
}
}
}
This subscriber is very similar to CatchSubscriber with only one difference is that CatchSubscriber returns original observable in selector method, in my case I want to return last value so the selector could use it to compose a brand new observable rather than reusing the original one.
But I messed with resubscribe logic somehow so the resulting observable never returns complete for small amount of test values, and crashes with stack overflow for bigger amount of test values.
Also, my idea here is to implement a new operator but if it's possible to implement it in a single method just using composition of existing operators, in a generic way, that would be even better :)
Example of an alternative method but without operator:
function observeStable<T, Q>(
query: Q,
continueQueryFunc: (query: Q, lastValue?: T) => Observable<T>
): Observable<T> {
return observeUnstable<T>(query).catch((err, ???) =>
if (err instanceof DisconnectedError) {
let lastValue = ???
let continueQuery = continueQueryFunc(query, lastValue);
return observeUnstable(continueQuery);
} else {
throw err;
}
);
}

Resources