In the following code, which is a simplified version of a more elaborate pipeline, "Done processing" is never called for 2.
Why is that?
I suspect this is a problem due to the demand, but I cannot figure out the cause.
Note that if I remove the combineLatest() or the compactMap(), the value 2 is properly processed (but I need these combineLatest and compactMap for correctness, in my real example they are more involved).
var cancellables = Set<AnyCancellable>([])
func process<T>(_ value: T) -> AnyPublisher<T, Never> {
return Future<T, Never> { promise in
print("Starting processing of \(value)")
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 0.1) {
promise(.success(value))
}
}.eraseToAnyPublisher()
}
let s = PassthroughSubject<Int?, Never>()
s
.print("Combine->Subject")
.combineLatest(Just(true))
.print("Compact->Combine")
.compactMap { value, _ in value }
.print("Sink->Compact")
.flatMap(maxPublishers: .max(1)) { process($0) }
.sink {
print("Done processing \($0)")
}
.store(in: &cancellables)
s.send(nil)
// Give time for flatMap to finish
Thread.sleep(forTimeInterval: 1)
s.send(2)
It sounds like a bug of combineLatest. When a downstream request additional demand "synchronously" (as per-print publisher output), that demand doesn't flow upstream.
One way to overcome this is to wrap the downstream of combineLatest in a flatMap:
s
.combineLatest(Just(true))
.flatMap(maxPublishers: .max(1)) {
Just($0)
.compactMap { value, _ in value }
.flatMap { process($0) }
}
.sink {
print("Done processing \($0)")
}
.store(in: &cancellables)
The outer flatMap now creates the back pressure, and the inner flatMap doesn't need it anymore.
Related
I'm trying to perform some iterative work, and use Combine to publish the progress (0.0 - 100.0) using a CurrentValueSubject, which my ViewModel will then subscribe to
(Edit: the ViewModel controls a SwiftUI ProgressView, which is why receive(on: DispatchQueue.main) is used)
What I'm seeing is that the outputs are being published, but sink doesn't receive any of them until the publisher has completed.
Here's a simplified example:
// Class that performs iterative calculations and publish its progress
class JobWorker {
private var subject: CurrentValueSubject<Double, Never>
private var progress = 0.0
init() {
self.subject = CurrentValueSubject<Double, Never>(progress)
}
func getPublisher() {
return subject.eraseToAnyPublisher()
}
func doWork() {
let tasks = [1,2,3,4,5]
tasks.forEach { num in
// ... perform some calculations ...
self.incrementProgress(20.0)
}
}
func incrementProgress(_ by: Double) {
progress += by
if progress >= 100.0 {
print("PUBLISH completion")
subject.send(completion: .finished)
} else {
print("PUBLISH value \(progress)")
subject.send(progress)
}
}
}
// ViewModel that subscribes to JobWorker's publisher and updates the progress in the view
final class JobViewModel: ObservableObject {
#Published var progress: Double = 0.0
private var cancellables = Set<AnyCancellable>()
private var jobWorker: JobWorker
init() {
self.jobWorker = JobWorker()
}
func runJob() {
self.jobWorker
.getPublisher()
.receive(on: DispatchQueue.main)
.handleEvents(
receiveSubscription: { _ in
print("RECEIVE subscription")
},
receiveOutput: { value in
print("RECEIVE output \(value)")
},
receiveCompletion: { _ in
print("RECEIVE completion")
},
receiveCancel: {
print("RECEIVE cancel")
},
receiveRequest: { _ in
print("RECEIVE demand")
}
)
.sink { [weak self] (completion) in
guard let self = self else { return }
print("SINK completion")
} receiveValue: { [weak self] (value) in
guard let self = self else { return }
print("SINK output \(value)")
self.progress = value
}
.store(in: &cancellables)
print("*** DO WORK ***")
self.jobWorker.doWork()
}
}
Calling JobViewModel.runJob results in the following output:
RECEIVE subscription
RECEIVE demand
RECEIVE output 0.0
SINK output 0.0
*** DO WORK ***
PUBLISH value 20.0
PUBLISH value 40.0
PUBLISH value 60.0
PUBLISH value 80.0
PUBLISH value 100.0
PUBLISH completion
RECEIVE output 20.0
SINK output 20.0
RECEIVE output 40.0
SINK output 40.0
RECEIVE output 60.0
SINK output 60.0
RECEIVE output 80.0
SINK output 80.0
RECEIVE output 100.0
SINK output 100.0
RECEIVE completion
SINK completion
After the CurrentValueSubject is first initialized, all of the outputs are published before handleEvents or sink receives anything.
Instead, I would have expected the output to show PUBLISH output x, RECEIVE output x, SINK output x for each of the outputs, followed by the completion.
The problem is that you are running your worker on the same thread where you are collecting the results.
Because you are doing a receive(on:) on the main DispatchQueue each value that passes through receive(on:) is roughly equivalent to putting a new block on the main queue to be executed when the queue is free.
Your worker fires up, executing synchronously on the main queue. While it's running, the main queue is tied up and not available for other work.
As the worker does its thing, it is publishing results to the subject, and as part of the publisher pipeline receive(on:) queues up the delivery of each result to the main queue, waiting for that queue to be free. The critical point, however, is that the main queue won't be free to handle those blocks, and report results, until the worker is done because the worker itself is tying up the main queue.
So none of your results are reported until after all the work is one.
I suspect what you want to do is run your work in a different context, off the main thread, so that it can complete asynchronously and only report the results on the main thread.
Here's a playground, based on your code, that does that:
import UIKit
import Combine
import PlaygroundSupport
class JobWorker {
private var subject = CurrentValueSubject<Double, Never>(0)
var publisher: AnyPublisher<Double, Never> {
get { subject.eraseToAnyPublisher() }
}
func doWork() async {
do {
for subtask in 1...5 {
guard !Task.isCancelled else { break }
print("doing task \(subtask)")
self.incrementProgress(by: 20)
try await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC)
}
} catch is CancellationError {
print("The Tasks were cancelled")
} catch {
print("An unexpected error occured")
}
}
private func incrementProgress(by: Double) {
subject.value = subject.value + by;
if subject.value >= 100 {
subject.send(completion: .finished)
}
}
}
let worker = JobWorker()
let subscription = worker.publisher
.print()
.receive(on: DispatchQueue.main)
.sink { _ in
print("done")
} receiveValue: { value in
print("New Value Received \(value)")
}
Task {
await worker.doWork()
}
PlaygroundPage.current.needsIndefiniteExecution = true
I made your doWork function an async function so I could execute it from an independent Task. I also added a delay because it makes the asynchronous nature of the code a bit easier to see.
In the "main thread, I create a JobWorker and subscribe to its publisher, but to do the work I create a task and run doWork in that separate task. Progress is reported in the main thread, but the work is being done (and completed) in a different execution context.
Just getting into Combine, and for some reason I can't get passthrough subjects to work. Even though I have copy-pasted examples from multiple different sources, they just won't print anything. I have tried with Publishers and CurrentValueSubjects and they work fine, but with PassThroughSubjects; nope. Here's an example that I have tried:
let mySubject = PassthroughSubject<String, Error>()
mySubject.sink(receiveCompletion: { completion in
print("-- completion", completion)
}, receiveValue: { value in
print("-- value", value)
}).cancel()
mySubject.send("one")
mySubject.send("two")
mySubject.send("three")
This is run in viewDidLoad.
What am I doing wrong?
Like I said, I have tried Publishers and CurrentValueSubjects with success:
["one", "two", "three"].publisher
.sink(receiveValue: { v in
print("-- hello", v)
}).cancel()
let subject = CurrentValueSubject<String, Error>("Initial Value")
subject.send("Hello")
subject.sink(receiveCompletion: { c in
print("-- completion", c)
}, receiveValue: { v in
print("-- value", v)
}).cancel()
The warning that you are seeing that the subscription is unused is a hint to store the token returned by sink like so:
let mySubject = PassthroughSubject<String, Error>()
let cancellable = mySubject
.sink(
receiveCompletion: { completion in
print("-- completion", completion)
},
receiveValue: { value in
print("-- value", value)
}
)
then when you call:
mySubject.send("one")
mySubject.send("two")
mySubject.send("three")
you will see this printed out:
-- value one
-- value two
-- value three
you can cancel the subscription if you are not longer interested in receiving updates:
cancellable.cancel()
or you can send a completion:
mySubject.send(completion: .finished)
and then you will see this printed out:
-- completion finished
Coming from rx I imagined .cancel() worked like .dispose(by:)
No, cancel() is like dispose(), not disposed(by:) in rx. You should not cancel first, then send things to the subject. And unlike a CurrentValueSubject, it doesn't remember the value you sent it, so you must send values to it after you sink, but before you cancel.
Just like how you would use a DisposeBag in rx, you should do this with a Set<AnyCancellable> in Combine:
var cancellables: Set<AnyCancellable> = []
The Combine counterpart of disposed(by:) is store(in:):
subject.sink(receiveCompletion: { c in
print("-- completion", c)
}, receiveValue: { v in
print("-- value", v)
}).store(in: &cancellables)
subject.send("Hello")
I have a BehaviorSubject where my tableview is bound to through RxDataSources.
Besides that, I have a pull to refresh which creates an observable that updates the data and updates the data in the BehaviorSubject so that my UITableView updates correctly.
Now the question is, how do I handle the error handling for whenever my API call fails?
Few options that I have thought of was:
Subscribe to the observer's onError and call the onError of my BehaviorSubject\
Somehow try to concat? or bind(to: ..)
Let another subscriber in my ViewController subscribe besides that my tableview subscribes to the BehaviorSubject.
Any suggestions?
Ideally, you wouldn't use the BehaviorSubject at all. From the Intro to Rx book:
The usage of subjects should largely remain in the realms of samples and testing. Subjects are a great way to get started with Rx. They reduce the learning curve for new developers, however they pose several concerns...
Better would be to do something like this in your viewDidLoad (or a function that is called from your viewDidLoad):
let earthquakeData = Observable.merge(
tableView.refreshControl!.rx.controlEvent(.valueChanged).asObservable(),
rx.methodInvoked(#selector(UIViewController.viewDidAppear(_:))).map { _ in }
)
.map { earthquakeSummary /* generate URLRequest */ }
.flatMapLatest { request in
URLSession.shared.rx.data(request: request)
.materialize()
}
.share(replay: 1)
earthquakeData
.compactMap { $0.element }
.map { Earthquake.earthquakes(from: $0) }
.map { $0.map { EarthquakeCellDisplay(earthquake: $0) } }
.bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: EarthquakeTableViewCell.self)) { _, element, cell in
cell.placeLabel.text = element.place
cell.dateLabel.text = element.date
cell.magnitudeLabel.text = element.magnitude
cell.magnitudeImageView.image = element.imageName.isEmpty ? UIImage() : UIImage(named: element.imageName)
}
.disposed(by: disposeBag)
earthquakeData
.compactMap { $0.error }
.map { (title: "Error", message: $0.localizedDescription) }
.bind { [weak self] title, message in
self?.presentAlert(title: title, message: message, animated: true)
}
.disposed(by: disposeBag)
The materialize() operator turns a Event.error(Error) result into an Event.next(.error(Error)) so that the chain won't be broken down. The .compactMap { $0.element } emits only the successful results while the .compactMap { $0.error } emits only the errors.
The above code is adapted from my RxEarthquake sample.
I'm using Combine and it happens to me many times that I have the need to emit Publishers with single values.
For example when I use flat map and I have to return a Publisher with a single value as an error or a single object I use this code, and it works very well:
return AnyPublisher<Data, StoreError>.init(
Result<Data, StoreError>.Publisher(.cantDownloadProfileImage)
)
This creates an AnyPublisher of type <Data, StoreError> and emits an error, in this case: .cantDownloadProfileImage
Here a full example how may usages of this chunk of code.
func downloadUserProfilePhoto(user: User) -> AnyPublisher<UIImage?, StoreError> {
guard let urlString = user.imageURL,
let url = URL(string: urlString)
else {
return AnyPublisher<UIImage?, StoreError>
.init(Result<UIImage?, StoreError>
.Publisher(nil))
}
return NetworkService.getData(url: url)
.catch({ (_) -> AnyPublisher<Data, StoreError> in
return AnyPublisher<Data, StoreError>
.init(Result<Data, StoreError>
.Publisher(.cantDownloadProfileImage))
})
.flatMap { data -> AnyPublisher<UIImage?, StoreError> in
guard let image = UIImage(data: data) else {
return AnyPublisher<UIImage?, StoreError>
.init(Result<UIImage?, StoreError>.Publisher(.cantDownloadProfileImage))
}
return AnyPublisher<UIImage?, StoreError>
.init(Result<UIImage?, StoreError>.Publisher(image))
}
.eraseToAnyPublisher()
}
Is there an easier and shorter way to create an AnyPublisher with a single value inside?
I think I should use the Just() object in somehow, but I can't understand how, because the documentation at this stage is very unclear.
The main thing we can do to tighten up your code is to use .eraseToAnyPublisher() instead of AnyPublisher.init everywhere. This is the only real nitpick I have with your code. Using AnyPublisher.init is not idiomatic, and is confusing because it adds an extra layer of nested parentheses.
Aside from that, we can do a few more things. Note that what you wrote (aside from not using .eraseToAnyPublisher() appropriately) is fine, especially for an early version. The following suggestions are things I would do after I have gotten a more verbose version past the compiler.
We can use Optional's flatMap method to transform user.imageURL into a URL. We can also let Swift infer the Result type parameters, because we're using Result in a return statement so Swift knows the expected types. Hence:
func downloadUserProfilePhoto(user: User) -> AnyPublisher<UIImage?, StoreError> {
guard let url = user.imageURL.flatMap({ URL(string: $0) }) else {
return Result.Publisher(nil).eraseToAnyPublisher()
}
We can use mapError instead of catch. The catch operator is general: you can return any Publisher from it as long as the Success type matches. But in your case, you're just discarding the incoming failure and returning a constant failure, so mapError is simpler:
return NetworkService.getData(url: url)
.mapError { _ in .cantDownloadProfileImage }
We can use the dot shortcut here because this is part of the return statement. Because it's part of the return statement, Swift deduces that the mapError transform must return a StoreError. So it knows where to look for the meaning of .cantDownloadProfileImage.
The flatMap operator requires the transform to return a fixed Publisher type, but it doesn't have to return AnyPublisher. Because you are using Result<UIImage?, StoreError>.Publisher in all paths out of flatMap, you don't need to wrap them in AnyPublisher. In fact, we don't need to specify the return type of the transform at all if we change the transform to use Optional's map method instead of a guard statement:
.flatMap({ data in
UIImage(data: data)
.map { Result.Publisher($0) }
?? Result.Publisher(.cantDownloadProfileImage)
})
.eraseToAnyPublisher()
Again, this is part of the return statement. That means Swift can deduce the Output and Failure types of the Result.Publisher for us.
Also note that I put parentheses around the transform closure because doing so makes Xcode indent the close brace properly, to line up with .flatMap. If you don't wrap the closure in parens, Xcode lines up the close brace with the return keyword instead. Ugh.
Here it is all together:
func downloadUserProfilePhoto(user: User) -> AnyPublisher<UIImage?, StoreError> {
guard let url = user.imageURL.flatMap({ URL(string: $0) }) else {
return Result.Publisher(nil).eraseToAnyPublisher()
}
return NetworkService.getData(url: url)
.mapError { _ in .cantDownloadProfileImage }
.flatMap({ data in
UIImage(data: data)
.map { Result.Publisher($0) }
?? Result.Publisher(.cantDownloadProfileImage)
})
.eraseToAnyPublisher()
}
import Foundation
import Combine
enum AnyError<O>: Error {
case forcedError(O)
}
extension Publisher where Failure == Never {
public var limitedToSingleResponse: AnyPublisher<Output, Never> {
self.tryMap {
throw AnyError.forcedError($0)
}.catch { error -> AnyPublisher<Output, Never> in
guard let anyError = error as? AnyError<Output> else {
preconditionFailure("only these errors are expected")
}
switch anyError {
case let .forcedError(publishedValue):
return Just(publishedValue).eraseToAnyPublisher()
}
}.eraseToAnyPublisher()
}
}
let unendingPublisher = PassthroughSubject<Int, Never>()
let singleResultPublisher = unendingPublisher.limitedToSingleResponse
let subscription = singleResultPublisher.sink(receiveCompletion: { _ in
print("subscription ended")
}, receiveValue: {
print($0)
})
unendingPublisher.send(5)
In the snippet above I am converting a passthroughsubject publisher which can send a stream of values into something that stops after sending the first value. The essence of the snippet in based on the WWDC session about introduction to combine https://developer.apple.com/videos/play/wwdc2019/721/ here.
We are esentially force throwing an error in tryMap and then catching it with a resolving publisher using Just which as the question states will finish after the first value is subscribed to.
Ideally the demand is better indicated by the subscriber.
Another slightly more quirky alternative is to use the first operator on a publisher
let subscription_with_first = unendingPublisher.first().sink(receiveCompletion: { _ in
print("subscription with first ended")
}, receiveValue: {
print($0)
})
I'm trying to test a very simple view model:
struct SearchViewModelImpl: SearchViewModel {
let query = PublishSubject<String>()
let results: Observable<BookResult<[Book]>>
init(searchService: SearchService) {
results = query
.distinctUntilChanged()
.throttle(0.5, scheduler: MainScheduler.instance)
.filter({ !$0.isEmpty })
.flatMapLatest({ searchService.search(query: $0) })
}
}
I'm trying to test receiving an error from service so I doubled it this way:
class SearchServiceStub: SearchService {
let erroring: Bool
init(erroring: Bool) {
self.erroring = erroring
}
func search(query: String) -> Observable<BookResult<[Book]>> {
if erroring {
return .just(BookResult.error(SearchError.downloadError, cached: nil))
} else {
return books.map(BookResult.success) // Returns dummy books
}
}
}
I'm testing a query that errors this way:
func test_when_searchBooksErrored_then_nextEventWithError() {
let sut = SearchViewModelImpl(searchService: SearchServiceStub(erroring: true))
let observer = scheduler.createObserver(BookResult<[Book]>.self)
scheduler
.createHotObservable([
Recorded.next(200, ("Rx")),
Recorded.next(800, ("RxSwift"))
])
.bind(to: sut.query)
.disposed(by: disposeBag)
sut.results
.subscribe(observer)
.disposed(by: disposeBag)
scheduler.start()
XCTAssertEqual(observer.events.count, 2)
}
To begin I'm just asserting if the count of events is correct but I'am only receiving one not two. I thought it was a matter of asynchronicity so I changed the test to use RxBlocking:
func test_when_searchBooksErrored_then_nextEventWithError() {
let sut = SearchViewModelImpl(searchService: SearchServiceStub(erroring: true))
let observer = scheduler.createObserver(BookResult<[Book]>.self)
scheduler
.createHotObservable([
Recorded.next(200, ("Rx")),
Recorded.next(800, ("RxSwift"))
])
.bind(to: sut.query)
.disposed(by: disposeBag)
sut.results.debug()
.subscribe(observer)
.disposed(by: disposeBag)
let events = try! sut.results.take(2).toBlocking().toArray()
scheduler.start()
XCTAssertEqual(events.count, 2)
}
But this never ends.
I don't know if there is something wrong with my stub, or maybe with the viewmodel, but the production app works correctly, emitting the events as the query fires.
Documentation of RxTest and RxBlocking is very very short, with the classic examples with a string or an integer, but nothing related with this kind of flow... it is very frustrating.
Your throttling your query with the MainScheduler.instance scheduler. Try removing that and see what happens. That is probably why your only getting one. You need to inject the test scheduler into that throttle when testing.
There are a few different ways to go about getting the right scheduler into your model. Based on your current code, dependency injection would work fine.
struct SearchViewModelImpl: SearchViewModel {
let query = PublishSubject<String>()
let results: Observable<BookResult<[Book]>>
init(searchService: SearchService, scheduler: SchedulerType = MainScheduler.instance) {
results = query
.distinctUntilChanged()
.throttle(0.5, scheduler: scheduler)
.filter({ !$0.isEmpty })
.flatMapLatest({ searchService.search(query: $0) })
}
}
then in your test:
let sut = SearchViewModelImpl(searchService: SearchServiceStub(erroring: true), scheduler: testScheduler)
Also, rather than using toBocking(), you can bind the results events to the testable Observer.
func test_when_searchBooksErrored_then_nextEventWithError() {
let sut = SearchViewModelImpl(searchService: SearchServiceStub(erroring: true), scheduler: testScheduler)
let observer = scheduler.createObserver(BookResult<[Book]>.self)
scheduler
.createHotObservable([
Recorded.next(200, ("Rx")),
Recorded.next(800, ("RxSwift"))
])
.bind(to: sut.query)
.disposed(by: disposeBag)
sut.results.bind(to: observer)
.disposed(by: disposeBag)
scheduler.start()
XCTAssertEqual(observer.events.count, 2)
}
Although toBlocking() can be useful in certain situation, you get a lot more information when you bind the events to a testableObserver.