switchToLatest in Combine doesn't behave as expected - swift

I've been trying to replicate flatMapLatest from RxSwift in Combine, I've read in a few places that the solution is to use .map(...).switchToLatest
I'm finding some differences between the two, and I'm not sure if it's my implementation/understanding which is the problem.
In RxSwift if the upstream observable emits a stop event (completed or error) then the downstream observables created in the flatMapLatest closure will continue to emit events until they themselves emit a stop event:
let disposeBag = DisposeBag()
func flatMapLatestDemo() {
let mockTrigger = PublishSubject<Void>()
let mockDataTask = PublishSubject<Void>()
mockTrigger
.flatMapLatest { mockDataTask }
.subscribe(onNext: { print("RECEIVED VALUE") })
.disposed(by: disposeBag)
mockTrigger.onNext(())
mockTrigger.onCompleted()
mockDataTask.onNext(()) // -> "RECEIVED VALUE" is printed
}
This same setup in Combine doesn't behave the same way:
var cancellables = Set<AnyCancellable>()
func switchToLatestDemo() {
let mockTrigger = PassthroughSubject<Void, Never>()
let mockDataTask = PassthroughSubject<Void, Never>()
mockTrigger
.map { mockDataTask }
.switchToLatest()
.sink { print("RECEIVED VALUE") }
.store(in: &cancellables)
mockTrigger.send(())
mockTrigger.send(completion: .finished)
mockDataTask.send(()) // -> Nothing is printed, if I uncomment the finished event above then "RECEIVED VALUE" is printed
}
Is this intentional? If so, how do we replicate the behaviour of flatMapLatest in Combine?
If it's not intentional, file a radar I guess?

I've using this Swift implementation by sergdort:
func flatMapLatest<T: Publisher>(_ transform: #escaping (Self.Output) -> T) -> Publishers.SwitchToLatest<T, Publishers.Map<Self, T>> where T.Failure == Self.Failure {
map(transform).switchToLatest()
}

Related

RxSwift event fired twice

I set up this sandbox with a piece of code I got combining internal and external actions. I simplified as much as possible to reproduce the issue.
import PlaygroundSupport
import RxSwift
class Sandbox {
let publisher: PublishSubject<Int>
private let disposeBag = DisposeBag()
init() {
self.publisher = PublishSubject()
let publisher2 = publisher.debug()
let publisher3 = PublishSubject<Int>()
let publisherMerge = PublishSubject.merge([publisher2, publisher3])
let operation = publisherMerge
.map { $0 + 2 }
let operation2 = operation
.map { $0 + 3 }
// Will never fire on the sandbox
operation
.filter { $0 < 0 }
.flatMapLatest(Sandbox.doSomething)
.subscribe { print("Operation ", $0) }
.disposed(by: disposeBag)
operation2
.subscribe { print("Operation2 ", $0) }
.disposed(by: disposeBag)
}
static func doSomething(value: Int) -> Observable<Int> {
return .just(value)
}
}
let disposeBag = DisposeBag()
let sandbox = Sandbox()
Observable<Int>
.interval(.seconds(3), scheduler: MainScheduler.instance)
.subscribe(onNext: { _ in sandbox.publisher.onNext(0) })
.disposed(by: disposeBag)
PlaygroundPage.current.needsIndefiniteExecution = true
I got confused as the events are triggered twice for some reason.
2022-11-07 21:16:15.292: Sandbox.playground:12 (init()) -> subscribed
2022-11-07 21:16:15.293: Sandbox.playground:12 (init()) -> subscribed
2022-11-07 21:16:18.301: Sandbox.playground:12 (init()) -> Event next(0)
2022-11-07 21:16:18.302: Sandbox.playground:12 (init()) -> Event next(0)
Operation2 next(5)
Any approach on why is this happening and how to ensure only 1 event if fired.
EDIT 2022-11-08T02:49:37+00:00
A better example to illustrate what I am trying to achieve:
import Foundation
import PlaygroundSupport
import RxSwift
struct ViewModel: Equatable {
enum State: Equatable {
case initialized
}
let state: State
}
enum Reducer {
enum Action {
case dummyReducerAction(String)
}
enum Effect {
case dummyEffect
}
struct State: Equatable {
let viewModel: ViewModel
let effect: Effect?
}
static func reduce(state: State, action: Action) -> State {
let viewModel: ViewModel = state.viewModel
let _: Effect? = state.effect
let viewModelState: ViewModel.State = viewModel.state
let noChange = State(viewModel: viewModel, effect: nil)
switch (action, viewModelState) {
case (.dummyReducerAction(let s), _):
print(s, Date())
return noChange
}
}
static func fromAction(_ action: Interactor.Action) -> Action {
switch action {
case .dummyAction:
return .dummyReducerAction("fromAction")
}
}
static func performAsyncSideEffect(effect: Effect) -> Observable<Action> {
switch effect {
case .dummyEffect:
return .just(.dummyReducerAction("performSideEffect"))
}
}
}
class Interactor {
enum Action {
case dummyAction
}
let action: PublishSubject<Action>
let viewModel: BehaviorSubject<ViewModel>
private let disposeBag = DisposeBag()
init() {
self.action = PublishSubject()
let initialViewModel = ViewModel(state: .initialized)
let initialReducerState = Reducer.State(viewModel: initialViewModel, effect: nil)
let externalAction = action.map(Reducer.fromAction).debug()
let internalAction = PublishSubject<Reducer.Action>()
let allActions = PublishSubject.merge([externalAction, internalAction])
self.viewModel = BehaviorSubject(value: initialViewModel)
let reducerState = allActions
.scan(initialReducerState, accumulator: Reducer.reduce)
let viewModelObservable = reducerState
.map { $0.viewModel }
.distinctUntilChanged()
reducerState
.compactMap { $0.effect }
.flatMapLatest(Reducer.performAsyncSideEffect)
.subscribe(internalAction)
.disposed(by: disposeBag)
viewModelObservable
.subscribe(onNext: viewModel.onNext)
.disposed(by: disposeBag)
}
}
let disposeBag = DisposeBag()
let interactor = Interactor()
Observable<Int>
.interval(.seconds(5), scheduler: MainScheduler.instance)
.subscribe(onNext: { _ in interactor.action.onNext(.dummyAction) })
.disposed(by: disposeBag)
PlaygroundPage.current.needsIndefiniteExecution = true
Even with share(), the reducer is fired twice and print the action content.
with share():
2022-11-08 11:46:05.298: Sandbox.playground:71 (init()) -> subscribed
2022-11-08 11:46:10.310: Sandbox.playground:71 (init()) -> Event next(dummyReducerAction("fromAction"))
fromAction 2022-11-08 02:46:10 +0000
fromAction 2022-11-08 02:46:10 +0000
without share()
2022-11-08 11:56:33.369: Sandbox.playground:71 (init()) -> subscribed
2022-11-08 11:56:33.370: Sandbox.playground:71 (init()) -> subscribed
2022-11-08 11:56:38.383: Sandbox.playground:71 (init()) -> Event next(dummyReducerAction("fromAction"))
fromAction 2022-11-08 02:56:38 +0000
2022-11-08 11:56:38.385: Sandbox.playground:71 (init()) -> Event next(dummyReducerAction("fromAction"))
fromAction 2022-11-08 02:56:38 +0000
Every subscription gets its own stream. Since you are subscribing twice to the operation observable (once through operation2 and once directly) you get two subscribe requests and two next events every time it emits a value.
If you don't want that, the solution is to use the .share() operator.
let operation = publisherMerge
.map { $0 + 2 }
.share()
I'm not sure that it's actually a problem though since there is no state depending on number of subscriptions that I can see.
EDIT
Again, the problem is the same, you just changed which thing is being observed twice.
Maybe it will help if I explain how I figure out where the share() should go.
Find the debug() statement. I see that it's on externalAction
Is externalAction being shared? No, it's only being used by allActions.
Is allActions being shared? No, it's only being used by reducerState.
Is reducerState being shared? Yes, it's being used by viewModelObservable and it's explicitly subscribed to.
Put the share on that observable.
let reducerState = allActions
.scan(initialReducerState, accumulator: Reducer.reduce)
.share()
No matter how complex the example gets, the solution is to put a share() (or share(replay: 1)) on whatever Observable(s) that is being used in more than one place.

Executing a task asynchronously using Combine with Swift

I recently started to study the Combine and ran into a certain problem.
First, I will describe what I am doing.
I trying to use Clean Architecture
Here you can see my Repository
protocol Repository {
func test()
}
class MockRepository: Repository {
func test() {
sleep(3)
}
}
Then I created UseCase
class UseCaseBase<TInput, TOutput> {
var task: TOutput? { return nil }
var repository: Repository
init(_ repository: Repository) {
self.repository = repository
}
func execute(with payload: TInput) -> AnyPublisher<TOutput, Never> {
return AnyPublisher(Future<TOutput, Never> { promise in
promise(.success(self.task!))
})
.eraseToAnyPublisher()
}
}
class MockUseCase: UseCaseBase<String, Int> {
override var task: Int? {
repository.test()
return 1
}
}
And then in a init block ContentView I did something like that
init() {
let useCase = MockUseCase(MockRepository())
var cancellables = Set<AnyCancellable>()
useCase.execute(with: "String")
.sink(receiveValue: { value in
print(value)
})
.store(in: &cancellables)
print("Started")
}
At first, I want to get
"Started"
and then after sleep(3)
value "1"
Now I get
"1" and then "Started"
Your sleep(3) call runs on the main thread, which means that it blocks any other operations, including the code that prints the "Started" text.
I won't be rambling about how bad it is to block the main thread, this is well known information, but this is the reason you see the behaviour you asked about.
I don't see any thread switching code in your question, so if you wish to achieve some kind of asynchronicity, then you can either go with Rob's solution of using dispatch(after:), or do the locomotion (the sleep) on another thread:
func execute(with payload: TInput) -> AnyPublisher<TOutput, Never> {
return AnyPublisher(Future<TOutput, Never> { promise in
DispatchQueue.global().async {
promise(.success(self.task!))
}
})
.eraseToAnyPublisher()
}
The first thing I'll mention is that you need to hold a reference to cancellables or your publisher will automatically be cancelled when you add async processing to the chain. Move it out of your init method and into a property.
You can also get rid of the sleep and simply chain to a Delay publisher on a queue of your choice. I chose main.
struct SomeThing {
var cancellables = Set<AnyCancellable>()
init() {
MockUseCase(MockRepository())
.execute(with: "String")
.delay(for: 3.0, scheduler: DispatchQueue.main)
.sink(receiveValue: { print($0) } )
.store(in: &cancellables)
print("Started")
}
}
class MockRepository: Repository {
func test() {
// sleep(3)
}
}
Another option is to get rid of the delay, get rid of the sleep and fulfill your promise asynchronously:
struct SomeThing {
var cancellables = Set<AnyCancellable>()
init() {
MockUseCase(MockRepository())
.execute(with: "String")
.sink(receiveValue: { print($0) } )
.store(in: &cancellables)
print("Started")
}
}
class MockRepository: Repository {
func test() {
// sleep(3)
}
}
func execute(with payload: TInput) -> AnyPublisher<TOutput, Never> {
return Future<TOutput, Never> { promise in
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
promise(.success(self.task!))
}
}
.eraseToAnyPublisher()
}

Unit Test for getting the PassthroughSubject publisher's value

Is there a way to get the value of a PassthroughSubject publisher in a Unit test?
I want to test that a function returns success and to test this one I want to see when the publisher value is .loaded, then is success.
class HomeViewModel: ObservableObject {
var homeState = PassthroughSubject<StatePublisher, Never>()
func load(item: HomeModel) {
self.homeState.send(.loading)
self.dataSource.load(item: item) { result in
switch result {
case .success:
self.homeState.send(.loaded)
case let .failure(error):
self.homeState.send(.error(message: error.localizedDescription))
}
}
}
}
class HomeViewModelTests: XCTestCase {
var sut: ViewModel!
var subscriptions = Set<AnyCancellable>()
override func setUpWithError() throws {
sut = ViewModel()
}
override func tearDownWithError() throws {
sut = nil
subscriptions = []
}
func testUpdateHomeSuccess() {
let expected = StatePublisher.loaded
var result = StatePublisher.loading
sut.load(item: HomeModel.fixture())
sut.homeState
.sink(receiveValue: { state in
result = state
})
.store(in: &subscriptions)
XCTAssert(
result == expected,
"Home expected to be \(expected) but was \(result)"
)
}
}
I tried a test like this, but sink is never called.
Presumably, dataSource.load operates asynchronously. That means you need to use an XCTestExpectation in your test case. Also, because you're using a PassthroughSubject rather than a CurrentValueSubject, you should create your subscription before you start the load, to be sure you can't miss any published values.
let ex = XCTestExpectation()
sut.homeState
.sink {
if case .loaded = $0 {
ex.fulfill()
}
}
.store(in: &subscriptions)
wait(for: [ex], timeout: 10)
// Test fails if it doesn't call `ex.fulfill()` within 10 seconds.
Read Apple's guide to Testing Asynchronous Operations with Expectations.
I changed my PassthroughSubject to CurrentValueSubject as then the state of the value is present also when executing the XCTestCase.
So this test was always failing: (the PassthroughSubject is declared in viewModel class which is tested)
var someValue: PassthroughSubject<Bool, Never> = .init()
func someTest() throws {
let exp = expectation(description: "Value changed")
viewModel.functionChangingTheValue()
viewModel.someValue.sink { result in
XCTAssertTrue(result)
exp.fulfill()
}
.store(in: &cancellables)
wait(for: [exp], timeout: 0.1)
}
But this one is now successful
var someValue: CurrentValueSubject<Bool, Never> = .init(false)
func someTest() throws {
let exp = expectation(description: "Value changed")
viewModel.functionChangingTheValue()
viewModel.someValue.sink { result in
XCTAssertTrue(result)
exp.fulfill()
}
.store(in: &cancellables)
wait(for: [exp], timeout: 0.1)
}

How can I trigger a process after a returned publisher would be subscribed?

I have a function that returns a publisher. This publisher gives the results of a background process. I only want to trigger the background process when the publisher would be subscribed, so that no results are lost. The background process can update its results many times, so the variant with Future is not suitable.
private let passthroughSubject = PassthroughSubject<Data, Error>()
// This function will be used outside.
func fetchResults() -> AnyPublisher<Data, Error> {
return passthroughSubject
.eraseToAnyPublisher()
.somehowTriggerTheBackgroundProcess()
}
extension MyModule: MyDelegate {
func didUpdateResult(newResult: Data) {
self.passthroughSubject.send(newResult)
}
}
What have I tried?
Future:
Future<Data, Error> { [weak self] promise in
self?.passthroughSubject
.sink(receiveCompletion: { completion in
// My logic
}, receiveValue: { value in
// My logic
})
.store(in: &self.cancellableSet)
self?.triggerBackgroundProcess()
}.eraseToAnyPublisher()
Works the way I want but the subscriber is called only once (logical).
Deffered:
Deferred<AnyPublisher<Data, Error>>(createPublisher: { [weak self] in
defer {
self?.triggerBackgroundProcess()
}
return passthroughSubject.eraseToAnyPublisher()
}
Debugger shows that everything is correct: first return then trigger but the subscriber is not called for the first time.
receiveSubscription:
passthroughSubject
.handleEvents(receiveSubscription: { [weak self] subscription in
self?.triggerBackgroundProcess()
})
.eraseToAnyPublisher()
The same effect as with Deffered.
Is it even possible what I want to achieve?
Or, it is better to create a public publisher subscribe it and receive results from background process. And the fetchResults() function doesn't return anything?
Thanks in advance for your help.
You can write your own type that conforms to Publisher and wraps a PassthroughSubject. In your implementation, you can start the background process when you get a subscription.
public struct MyPublisher: Publisher {
public typealias Output = Data
public typealias Failure = Error
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Output, Downstream.Failure == Failure
{
let subject = PassthroughSubject<Output, Failure>()
subject.subscribe(subscriber)
startBackgroundProcess(subject: subject)
}
private func startBackgroundProcess(subject: PassthroughSubject<Output, Failure>) {
DispatchQueue.global(qos: .utility).async {
print("background process running")
subject.send(Data())
subject.send(completion: .finished)
}
}
}
Note that this publisher starts a new background process for each subscriber. That is a common implementation. For example URLSession.DataTaskPublisher issues a new request for each subscriber. If you want multiple subscribers to share the output of a single request, you can use the .multicast operator, add multiple subscribers, and then .connect() the multicast publisher to start the background process once:
let pub = MyPublisher().multicast { PassthroughSubject() }
pub.sink(...).store(in: &tickets) // first subscriber
pub.sink(...).store(in: &tickets) // second subscriber
pub.connect().store(in: &tickets) // start the background process
It seems to me that your last bit of code is a perfectly viable solution: don't trigger the background process until you detect the subscription. Example:
let subject = PassthroughSubject<String, Never>()
var storage = Set<AnyCancellable>()
func start() {
self.subject
.handleEvents(receiveSubscription: {_ in
print("subscribed")
DispatchQueue.main.async {
self.doSomethingAsynchronous()
}
})
.sink { print("got", $0) }
.store(in: &storage)
}
func doSomethingAsynchronous() {
DispatchQueue.global(qos: .userInitiated).async {
DispatchQueue.main.async {
self.subject.send("bingo")
}
}
}

How to properly manage a collection of `AnyCancellable`

I'd like all publishers to execute unless explicitly cancelled. I don't mind AnyCancellable going out of scope, however based on docs it automatically calls cancel on deinit which is undesired.
I've tried to use a cancellable bag, but AnyCancelable kept piling up even after the publisher fired a completion.
Should I manage the bag manually? I had impression that store(in: inout Set) was meant to be used for convenience of managing the cancellable instances, however all it does is push AnyCancellable into a set.
var cancelableSet = Set<AnyCancellable>()
func work(value: Int) -> AnyCancellable {
return Just(value)
.delay(for: .seconds(1), scheduler: DispatchQueue.global(qos: .default))
.map { $0 + 1 }
.sink(receiveValue: { (value) in
print("Got value: \(value)")
})
}
work(value: 1337).store(in: &cancelableSet)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) {
print("\(cancelableSet)")
}
What I came up with so far, which works fine but makes me wonder if something is missing in the Combine framework or it was not meant to be used in such fashion:
class DisposeBag {
private let lock = NSLock()
private var cancellableSet = Set<AnyCancellable>()
func store(_ cancellable: AnyCancellable) {
print("Store cancellable: \(cancellable)")
lock.lock()
cancellableSet.insert(cancellable)
lock.unlock()
}
func drop(_ cancellable: AnyCancellable) {
print("Drop cancellable: \(cancellable)")
lock.lock()
cancellableSet.remove(cancellable)
lock.unlock()
}
}
extension Publisher {
#discardableResult func autoDisposableSink(disposeBag: DisposeBag, receiveCompletion: #escaping ((Subscribers.Completion<Self.Failure>) -> Void), receiveValue: #escaping ((Self.Output) -> Void)) -> AnyCancellable {
var sharedCancellable: AnyCancellable?
let disposeSubscriber = {
if let sharedCancellable = sharedCancellable {
disposeBag.drop(sharedCancellable)
}
}
let cancellable = handleEvents(receiveCancel: {
disposeSubscriber()
}).sink(receiveCompletion: { (completion) in
receiveCompletion(completion)
disposeSubscriber()
}, receiveValue: receiveValue)
sharedCancellable = cancellable
disposeBag.store(cancellable)
return cancellable
}
}
The subscriptions in Apple Combine are scoped in a RAII compliant fashion. I.e. the event of deinitialization is equivalent to the event of automatic disposal of the observable. That is contrary to RxSwift Disposable where this behavior is sometimes reproduced, but not strictly so.
Even in RxSwift if you lose a DisposeBag your subscriptions will be disposed and this is a feature. If you would like your subscription to live through the scope, it means that it belongs to an outer scope.
And none of these implementations get busy actually tossing out the Disposables out of the retention tree once the subscriptions are done.