Swift Combine: combining three signals into one - ios

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.)

Related

Premature completion of publisher in flatMap in Combine

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.

Cancel firebase function

Is it possible to cancel Firebase HTTPS Callable function during it's request?
I have a function with some predictive search. It is called each time when user inputs a character in a search field.
Code:
func startSearch(_ query: String, completion: #escaping (_ results: [SearcheResults]) -> Void) {
let data = [
"query": query
]
functions.httpsCallable("startSearch").call(data) { (result, error) in
if error != nil {
completion([])
} else if let data = result?.data {
// some data manipulations
completion(elements)
}
}
}
Or maybe somehow dismiss earlier completions? Because for now, if user is very rapid and enter text, for example "Berlin" - completion will fire 6 times. I'd like to have a way to cancel a function or cancel previous completions.
Thanks in advance.
You should try debounce, basically in debounce before you fire a request you wait for short span(eg:- 2 secs), and if user types in that span again, timer is reset to again 2 secs,check the link
Once the call is made it cannot be cancelled, It will Either get executed to completition or timedout.
Once you invoke a callable function, it can't be canceled. The function will run to completion or timeout. You will need to be sure on the client that you really want to invoke the function. You are by no means obliged to consume the result (you can ignore it if you want), but the transaction will complete, unless the client app dies in the process. In that case, the function on the backend will still complete, but it will just not be able to deliver the response.

Make multiple asynchronous requests but wait for only one

I have a question concerning asynchronous requests. I want to request data from different sources on the web. Each source might have the data I want but I do not know that beforehand. Because I only want that information once, I don't care about the other sources as soon as one source has given me the data I need. How would I go about doing that?
I thought about doing it with a didSet and only setting it once, something like this:
var dogPicture : DogPicture? = nil {
didSet {
// Do something with the picture
}
}
func findPictureOfDog(_ sources) -> DogPicture? {
for source in sources {
let task = URL.Session.shared.dataTask(with: source) { (data, response, error) in
// error handling ...
if data.isWhatIWanted() && dogPicture == nil {
dogPicture = data.getPicture()
}
}
task.resume()
}
}
sources = ["yahoo.com", "google.com", "pinterest.com"]
findPictureOfDog(sources)
However it would be very helpful, if I could just wait until findPictureOfDog() is finished, because depending on if I find something or not, I have to ask the user for more input.
I don't know how I could do it in the above way, because if I don't find anything the didSet will never be called, but I should ask the user for a picture then.
A plus: isWhatIWanted() is rather expensive, so If there was a way to abort the execution of the handler once I found a DogPicture would be great.
I hope I made myself clear and hope someone can help me out with this!
Best regards and thank you for your time
A couple of things:
First, we’re dealing with asynchronous processes, so you shouldn’t return the DogPicture, but rather use completion handler pattern. E.g. rather than:
func findPictureOfDog(_ sources: [String]) -> DogPicture? {
...
return dogPicture
}
You instead would probably do something like:
func findPictureOfDog(_ sources: [String], completion: #escaping (Result<DogPicture, Error>) -> Void) {
...
completion(.success(dogPicture))
}
And you’d call it like:
findPictureOfDog(sources: [String]) { result in
switch result {
case .success(let dogPicture): ...
case .failure(let error): ...
}
}
// but don’t try to access the DogPicture or Error here
While the above was addressing the “you can’t just return value from asynchronous process”, the related observations is that you don’t want to rely on a property as the trigger to signal when the process is done. All of the “when first process finishes” logic should be in the findPictureOfDog routine, and call the completion handler when it’s done.
I would advise against using properties and their observers for this process, because it begs questions about how one synchronizes access to ensure thread-safety, etc. Completion handlers are unambiguous and avoid these secondary issues.
You mention that isWhatIWanted is computationally expensive. That has two implications:
If it is computationally expensive, then you likely don’t want to call that synchronously inside the dataTask(with:completionHandler:) completion handler, because that is a serial queue. Whenever dealing with serial queues (whether main queue, network session serial queue, or any custom serial queue), you often want to get in and out as quickly as possible (so the queue is free to continue processing other tasks).
E.g. Let’s imagine that the Google request came in first, but, unbeknownst to you at this point, it doesn’t contain what you wanted, and the isWhatIWanted is now slowly checking the result. And let’s imagine that in this intervening time, the Yahoo request that came in. If you call isWhatIWanted synchronously, the result of the Yahoo request won’t be able to start checking its result until the Google request has failed because you’re doing synchronous calls on this serial queue.
I would suggest that you probably want to start checking results as they came in, not waiting for the others. To do this, you want a rendition of isWhatIWanted the runs asynchronously with respect to the network serial queue.
Is the isWhatIWanted a cancelable process? Ideally it would be, so if the Yahoo image succeeded, it could cancel the now-unnecessary Pinterest isWhatIWanted. Canceling the network requests is easy enough, but more than likely, what we really want to cancel is this expensive isWhatIWanted process. But we can’t comment on that without seeing what you’re doing there.
But, let’s imagine that you’re performing the object classification via VNCoreMLRequest objects. You might therefore cancel any pending requests as soon as you find your first match.
In your example, you list three sources. How many sources might there be? When dealing with problems like this, you often want to constrain the degree of concurrency. E.g. let’s say that in the production environment, you’d be querying a hundred different sources, you’d probably want to ensure that no more than, say, a half dozen running at any given time, because of the memory and CPU constraints.
All of this having been said, all of these considerations (asynchronous, cancelable, constrained concurrency) seem to be begging for an Operation based solution.
So, in answer to your main question, the idea would be to write a routine that iterates through the sources, and calling the main completion handler upon the first success and make sure you prevent any subsequent/concurrent requests from calling the completion handler, too:
You could save a local reference to the completion handler.
As soon as you successfully find a suitable image, you can:
call that saved completion handler;
nil your saved reference (so in case you have other requests that have completed at roughly the same time, that they can’t call the completion handler again, eliminating any race conditions); and
cancel any pending operations so that any requests that have not finished will stop (or have not even started yet, prevent them from starting at all).
Note, you’ll want to synchronize the the above logic, so you don’t have any races in this process of calling and resetting the completion handler.
Make sure to have a completion handler that you call after all the requests are done processing, in case you didn’t end up finding any dogs at all.
Thus, that might look like:
func findPictureOfDog(_ sources: [String], completion: #escaping DogPictureCompletion) {
var firstCompletion: DogPictureCompletion? = completion
let synchronizationQueue: DispatchQueue = .main // note, we could have used any *serial* queue for this, but main queue is convenient
let completionOperation = BlockOperation {
synchronizationQueue.async {
// if firstCompletion not nil by the time we get here, that means none of them matched
firstCompletion?(.failure(DogPictureError.noneFound))
}
print("done")
}
for source in sources {
let url = URL(string: source)!
let operation = DogPictureOperation(url: url) { result in
if case .success(_) = result {
synchronizationQueue.async {
firstCompletion?(result)
firstCompletion = nil
Queues.shared.cancelAllOperations()
}
}
}
completionOperation.addDependency(operation)
Queues.shared.processingQueue.addOperation(operation)
}
OperationQueue.main.addOperation(completionOperation)
}
So what might that DogPictureOperation might look like? I might create an asynchronous custom Operation subclass (I just subclass a general purpose AsynchronousOperation subclass, like the one here) that will initiate network request and then run an inference on the resulting image upon completion. And if canceled, it would cancel the network request and/or any pending inferences (pursuant to point 3, above).
If you care about only one task use a completion handler, call completion(nil) if no picture was found.
var dogPicture : DogPicture?
func findPictureOfDog(_ sources, completion: #escaping (DogPicture?) -> Void) {
for source in sources {
let task = URL.Session.shared.dataTask(with: source) { (data, response, error) in
// error handling ...
if data.isWhatIWanted() && dogPicture == nil {
let picture = data.getPicture()
completion(picture)
}
}
task.resume()
}
}
sources = ["yahoo.com", "google.com", "pinterest.com"]
findPictureOfDog(sources) { [weak self] picture in
if let picture = picture {
self?.dogPicture = picture
print("picture set")
} else {
print("No picture found")
}
}
You can use DispatchGroup to run a check when all of your requests have returned:
func findPictureOfDog(_ sources: [String]) -> DogPicture? {
let group = DispatchGroup()
for source in sources {
group.enter()
let task = URLSession.shared.dataTask(with: source) { (data, response, error) in
// error handling ...
if data.isWhatIWanted() && dogPicture == nil {
dogPicture = data.getPicture()
}
group.leave()
}
task.resume()
}
group.notify(DispatchQueue.main) {
if dogPicture == nil {
// all requests came back but none had a result.
}
}
}

URLSession's dataTaskPublisher ends with a 'Cancelled' response vs regular completion. Why? [duplicate]

When trying to make a network request, I'm getting an error
finished with error [-999] Error Domain=NSURLErrorDomain Code=-999 "cancelled"
If I use URLSession.shared.dataTask instead of URLSession.shared.dataTaskPublisher it will work on IOS 13.3.
Here is my code :
return URLSession.shared.dataTaskPublisher(for : request).map{ a in
return a.data
}
.decode(type: MyResponse.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
This code worked on IOS 13.2.3.
You have 2 problems here:
1. like #matt said, your publisher isn't living long enough. You can either store the AnyCancellable as an instance var, or what I like to do (and appears to be a redux best practice) is use store(in:) to a Set<AnyCancellable> to keep it around and have it automatically cleaned up when the object is dealloced.
2. In order to kick off the actual network request you need to sink or assign the value.
So, putting these together:
var cancellableSet: Set<AnyCancellable> = []
func getMyResponse() {
URLSession.shared.dataTaskPublisher(for : request).map{ a in
return a.data
}
.decode(type: MyResponse.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.replaceError(with: MyResponse())
.sink { myResponse in print(myResponse) }
.store(in: &cancellableSet)
}
You have not shown enough code, but based on the symptom it is clear what the problem is: your publisher / subscriber objects are not living long enough. I would venture to say that your code was always wrong and it was just a quirk that it seemed to succeed. Make sure that your publisher and especially your subscriber are retained in long-lived objects, such as instance properties, so that the network communication has time to take place.
Here's a working example of how to use a data task publisher:
class ViewController: UIViewController {
let url = URL(string:"https://apeth.com/pep/manny.jpg")!
lazy var pub = URLSession.shared.dataTaskPublisher(for: url)
.compactMap {UIImage(data: $0.data)}
.receive(on: DispatchQueue.main)
var sub : AnyCancellable?
override func viewDidLoad() {
super.viewDidLoad()
let sub = pub.sink(receiveCompletion: {_ in}, receiveValue: {print($0)})
self.sub = sub
}
}
That prints <UIImage:0x6000008ba490 anonymous {180, 206}>, which is correct (as you can see by going to that URL yourself).
The point I'm making is that if you don't say self.sub = sub, you get exactly the error you are reporting: the subscriber sub, which is merely a local, goes out of existence immediately and the network transaction is prematurely cancelled (with the error you reported).
EDIT I think that code was written before the .store(in:) method existed; if I were writing it today, I'd use that instead of a sub property. But the principle is the same.
I needed to move my cancellable set "above" the scope of the function where my subscriber was executing. This worked fine in iOS 13.2 when the cancellable set had the same scope as the function of the subscriber, but stop working in 13.3. The dataTaskPublisher cancels with the error sited above. It makes sense that the cancellable set should "out live" the subscriber. Developer error. Lesson learned.

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