Premature completion of publisher in flatMap in Combine - ios

I have this minimal example:
import UIKit
import Combine
var values = [1,2,3,4,5]
var cancel = values.publisher
.delay(for: 0.1, scheduler: DispatchQueue.global())
.print()
.flatMap() { i in
[i].publisher.first()
}
.sink { completion in
print("Received Completion: \(completion)")
} receiveValue: { v in
print("Received Value: \(v)")
}
My expectation is that the source publisher emits the values from 1 to 5 into the stream. Each number gets transformed into (just for the sake of it) a new publisher that emits exactly the first value and then completes. Since this is done with each number, I would expect that all values reach the sink. This is not the case, however. Output looks like this:
request unlimited
receive value: (1)
Received Value: 1
receive value: (2)
Received Value: 2
receive value: (4)
Received Value: 4
receive finished
Received Completion: finished
receive value: (3)
receive value: (5)
In fact, only 3 values reach the sink before the completion event arrives. Why is this? The documentation states:
successful completion of the new Publisher does not complete the overall stream.
Even more curious, when you replace .flatMap() for .flatMap(maxPublishers: .max(1)) and add a .share() to the original source publisher only the first value makes it to the sink.
Any pointers are much appreciated!

Your use of DispatchQueue.global() is the problem. The values.publisher sends all of its outputs, and its completion, downstream to the delay operator as soon as values.publisher receives the subscription. The delay operator schedules six blocks (five for the output numbers and one for the completion) to run on DispatchQueue.global() 0.1 seconds later.
DispatchQueue.global() is a concurrent queue. This means that it can run any number of blocks simultaneously. So there is no guarantee which of the six scheduled blocks will finish first.
It is in general a bad idea to use a concurrent queue as a Combine scheduler. You should use a serial queue instead. Here's a simple example:
var cancel = values.publisher
.delay(for: 0.1, scheduler: DispatchQueue(label: "my queue"))
.print()
.flatMap() { i in
[i].publisher.first()
}
.sink { completion in
print("Received Completion: \(completion)")
} receiveValue: { v in
print("Received Value: \(v)")
}
But you probably want to create the queue once and store it in a property, so you can use it with multiple publishers.
If you actually want the outputs to be delivered on the main queue (perhaps because you're going to use them to update a view), you should just use DispatchQueue.main.

Related

How to receive continuous stream of states from two publishers working together

I have a function getState(), the purpose of which is to return a continuous stream of States. I have two publishers: statePublisher and requestPipeline.
When I call getState(), the requestPipeline is sent a Request. As the pipeline progresses, it returns states to the statePublisher.
This statePublisher gets referenced strongly during the operator .doThingThree(statePublisher). This operator presents a View Controller which holds on to the statePublisher. When that view controller dismisses, it sends a State and then completes.
This is my current, broken, code:
func getState(request: Request) -> AnyPublisher<State, Never> {
let statePublisher = PassthroughSubject<State, Never>()
let requestPipeline = PassthroughSubject<Request, Error>()
requestPublisher = requestPipeline
.eraseToAnyPublisher()
.doThingOne()
.doThingTwo(statePublisher)
.doThingThree(statePublisher)
requestPipeline.send(request)
return statePublisher
.eraseToAnyPublisher()
}
func getMyState() {
cancellable = getState(request: myRequest)
.sink { state in
print(state) // Never gets fired
}
}
Unfortunately calling getMyState() doesn't do anything. Do I need to subscribe to the requestPublisher before I can do anything? Any help appreciated!
You do have to subscribe to a PassthroughSubject before you send any values through it in order to receive events.
In your code you call getState.
getState calls send.
The send is ignored because there are no subscribers to requestPipeline.
Then getState returns.
You then you subscribe to statePublisher with sink.
Your code doesn't show any values ever being sent to statePublisher.
If there are were events being sent in doThingTwo and doThingThree then they are sent long before sink subscribes to statePublisher.
But then again doThingTwo and doThingThree are never invoked in the code that you've shown because there are no subscribers to requestPublisher or requestPipeline
If you are unsure if your publishers are executing use the .print(<name>) operator to see subscriptions and values passing through the pipeline.

Creating a sequential publisher in Swift Combine

I want to debounce a batch of events and process them after a delay of ~1.5 seconds. Here's what I've done.
class ViewModel: ObservableObject {
#Published var pending: [TaskInfo]
private var cancellable: AnyCancellable? = nil
init() {
processPendingTasks()
}
func queueTask(task: TaskInfo) {
pending.append(task)
}
private func processPendingTasks() {
cancellable = $pendingTasks
.debounce(for: 1.5, scheduler: RunLoop.main)
.sink(receiveValue: { batch in
// Iterate though elements and process events.
})
}
}
Issue: This works fine, but the issue that I've is that it performs unnecessary view updates since the array is tagged #Published.
What I'm looking for: The ideal approach would be a streaming setup where I get all events (in a batched fashion) but the sink should wait exactly for 1.5 seconds after the last event was added.
I tried PassthroughSubject, but it seems like it only gets me the last event that happened in the last 1.5 seconds.
A possible solution is a combination of a PassthroughSubject and the collect operator. In queueTask send the tasks to the subject.
func queueTask(task: TaskInfo) {
subject.send(task)
}
1.5 seconds after receiving the last item send
subject.send(completion: .finished)
and subscribe
subject
.collect()
.sink { [weak self] tasks in
self?.pending = tasks
}
If the interval of the incoming tasks is < 1.5 seconds you could also use the .timeout(1.5) operator which terminates the pipeline after the timeout interval.

Swift Combine: combining three signals into one

I'm dealing with a legacy libraries where I'm not at liberty to modify their code, and am trying to use Combine to weave them into something more easy to use. My situation is that a method call can either return a response, or a response and two notifications. The response-only is a success scenario, the response + 2 notifications is an error scenario. I want to combine both response and payload from the two notifications into an error that I can pass on to my app. The really fun thing is that I don't have a guarantee if the response or notifications come first, nor which of the notifications comes first. The notifications come in on a different thread than the response. The good thing is that they come in "just about the same time".
For handling a notification, I do
firstNotificationSink = notificationCenter.publisher(for: .firstErrorPart, object: nil)
.sink { [weak self] notification in
// parse and get information about the error
}
secondNotificationSink = notificationCenter.publisher(for: .secondErrorPart, object: nil)
.sink { [weak self] notification in
// parse and get more information about the error
}
and asking the legacy library for a response is:
func doJob() -> String {
let resultString = libDoStuff(reference)
}
Is there a way for me to use Combine to merge these three signals into one, given i.e. a 50ms timeframe? Meaning, if I get the result and two notifications, I have an error response I can pass on to my app, and if I have only the result and no notifications arrived in 50ms, then I can pass that success response to my app?
The part about combining the three signals is easy: use .zip. That's not very interesting. The interesting part of the problem is that you want a pipeline that signals whether a notification arrived within a certain time limit. Here's an example of how to do that (I'm not using your actual numbers, it's just a demo):
import UIKit
import Combine
enum Ooops : Error { case oops }
class ViewController: UIViewController {
var storage = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
print("start")
NotificationCenter.default.publisher(for: Notification.Name("yoho"))
.map {_ in true}
.setFailureType(to: Ooops.self)
.timeout(0.5, scheduler: DispatchQueue.main) { Ooops.oops }
.replaceError(with: false)
.sink {print($0)}
.store(in: &self.storage)
DispatchQueue.main.asyncAfter(deadline:.now()+0.2) {
NotificationCenter.default.post(name: Notification.Name("yoho"), object: self)
}
}
}
If the asyncAfter delay is 0.2, we get true (followed by false, but that's not important; we could change that if we wanted to). If the delay is 0.9, we get false. So the point is, the first value we get distinguishes correctly whether we got a signal in the required time.
Okay, so the rest is trivial: you just hook up your three signals with .zip, as I said before. It emits a tuple after all three publishers have emitted their first signal — and that's all the information you need, because you've got the result from the method call plus Bools that tell you whether the notifications arrived within the time limit. You can now read that tuple and analyze it, and do whatever you like. The .zip operator has a map function so you can emit the result of your analysis in good order. (If you wanted to transform the result of the map function into an error, that would require a further operator, but again, that's easy.)

Get the latest result from DispatchGroup wait

Problem Desctiption:
I want to do a bunch of asynchronous tasks by 'DispatchGroup' and when all of them finished it returned the result. In addition, I want to set timeout that limits the process and send me back the successful results by that time. I used the following structure:
Code Block
let myGroup = DispatchGroup()
var result = [Data]()
for i in 0 ..< 5 {
myGroup.enter()
Alamofire.request("https://httpbin.org/get", parameters: ["foo": "bar"]).responseJSON { response in
print("Finished request \(i)")
result.append(response.data)
myGroup.leave()
}
}
// Timeout for 10 seconds
myGroup.wait(timeout: DispatchTime(uptimeNanoseconds: 10000000000))
myGroup.notify(queue: .main) {
return result
}
How can I get the latest result if timeout happened?
Ok, so you are correctly using the enter/leave functionality of the DispatchGroup, but are having trouble with how to access the results of these. I think you are going wrong by trying to use both wait and notify, these two functions provide two different pieces of functionality not usually used together. After having setup up your work items, as you have done, you have two options:
The wait approach
This function blocks the calling queue and wait synchronously for either, the passed in wall time to elapse, or all work items in the group to leave. Because it is blocking the caller, it is important to always have a timeout in this function.
The notify approach
The function takes a target queue, and a block to be run when all work items in your group have completed. Here you are basically asking the system to notify you, asynchronously once all work items have been completed. Since this is asynchronous we are usually less worried about the timeout, it's not blocking anything.
Asynchronous wait (this appears to be what you want?)
If, as it seems you do, we want to be notified once all work items are complete, but also have a timeout, we have to do this ourselves, and it's not all that tricky. We can add a simple extension for the DispatchGroup class...
extension DispatchGroup {
func notifyWait(target: DispatchQueue, timeout: DispatchTime, handler: #escaping (() -> Void)) {
DispatchQueue.global(qos: .default).async {
_ = self.wait(timeout: timeout)
target.async {
handler()
}
}
}
}
This simple function dispatches asynchronously on a global background queue, then calls wait, which will wait for all work items to complete, or the specified timeout, whichever comes first. Then it will call back to your handler on the specified queue.
So that's the theory, how can you use this. We can keep your initial setup exactly the same
let myGroup = DispatchGroup()
var result = [Data]()
for i in 0 ..< 5 {
myGroup.enter()
Alamofire.request("https://httpbin.org/get", parameters: ["foo": "bar"]).responseJSON { response in
print("Finished request \(i)")
result.append(response.data)
myGroup.leave()
}
}
and then use our new function to wait for the end
myGroup.notifyWait(target: .main,
timeout: DispatchTime.now() + 10) {
// here you can access the `results` list, with any data that has
// been appended by the work items above before the timeout
// was reached
}

ReactiveCocoa 4 - Delaying and filtering signal events

I am implementing a search textfield using ReactiveCocoa 4, and want to only hit the search API after no text has been inputted for X amount of time. I have done this previously by canceling previously scheduled and firing off a "executeSearch" selector in the textDidChange delegate method. This ensures that every time I type, any previously scheduled "executeSearch" selector is canceled, and a new one is scheduled to fire in X seconds.
I now want to do this same behavior, but from a signal producer bound to my input text. My current implementation is close, but not the same. This behavior merely throttles the text input event to only fire every 0.5 seconds, instead of canceling the previous event.
searchTextInput.producer.delay(0.3, onScheduler: RACScheduler.currentScheduler())
.throttle(0.5, onScheduler: RACScheduler.currentScheduler())
.producer.startWithNext({ [unowned self] searchText in
self.executeSearch(searchText)
})
I'm having a hard time sifting through the ReactiveCocoa 4 documentation to know which signal functions I should be using! Thank you!
You need use DateSchedulerType. For example:
textField.rac_textSignal()
.toSignalProducer()
.map { $0 as! String }
.flatMapError { _ in SignalProducer<String, NoError>.empty }
.throttle(2.0, onScheduler: QueueScheduler.mainQueueScheduler)
.filter { $0.isEmpty }
.startWithNext { text in
print("t: \(text)")
}
Also you can write your executeSearch as SignalProducer and use flatMap(.Latest) for create signal-chains.
And don't forget using mainQueueSheduler for get result to UI

Resources