How to replicate PromiseKit-style chained async flow using Combine + Swift
This is not a real answer to your whole question — only to the part about how to get started with Combine. I'll demonstrate how to chain two asynchronous operations using the Combine framework:
print("start")
Future<Bool,Error> { promise in
delay(3) {
promise(.success(true))
}
}
.handleEvents(receiveOutput: {_ in print("finished 1")})
.flatMap {_ in
Future<Bool,Error> { promise in
delay(3) {
promise(.success(true))
}
}
}
.handleEvents(receiveOutput: {_ in print("finished 2")})
.sink(receiveCompletion: {_ in}, receiveValue: {_ in print("done")})
.store(in:&self.storage) // storage is a persistent Set<AnyCancellable>
First of all, the answer to your question about persistence is: the final subscriber must persist, and the way to do this is using the .store
method. Typically you'll have a Set<AnyCancellable>
as a property, as here, and you'll just call .store
as the last thing in the pipeline to put your subscriber in there.
Next, in this pipeline I'm using .handleEvents
just to give myself some printout as the pipeline moves along. Those are just diagnostics and wouldn't exist in a real implementation. All the print
statements are purely so we can talk about what's happening here.
So what does happen?
start
finished 1 // 3 seconds later
finished 2 // 3 seconds later
done
So you can see we've chained two asynchronous operations, each of which takes 3 seconds.
How did we do it? We started with a Future, which must call its incoming promise
method with a Result as a completion handler when it finishes. After that, we used .flatMap
to produce another Future and put it into operation, doing the same thing again.
So the result is not beautiful (like PromiseKit) but it is a chain of async operations.
Before Combine, we'd have probably have done this with some sort of Operation / OperationQueue dependency, which would work fine but would have even less of the direct legibility of PromiseKit.
Slightly more realistic
Having said all that, here's a slightly more realistic rewrite:
var storage = Set<AnyCancellable>()
func async1(_ promise:@escaping (Result<Bool,Error>) -> Void) {
delay(3) {
print("async1")
promise(.success(true))
}
}
func async2(_ promise:@escaping (Result<Bool,Error>) -> Void) {
delay(3) {
print("async2")
promise(.success(true))
}
}
override func viewDidLoad() {
print("start")
Future<Bool,Error> { promise in
self.async1(promise)
}
.flatMap {_ in
Future<Bool,Error> { promise in
self.async2(promise)
}
}
.sink(receiveCompletion: {_ in}, receiveValue: {_ in print("done")})
.store(in:&self.storage) // storage is a persistent Set<AnyCancellable>
}
As you can see, the idea that is our Future publishers simply have to pass on the promise
callback; they don't actually have to be the ones who call them. A promise
callback can thus be called anywhere, and we won't proceed until then.
You can thus readily see how to replace the artificial delay
with a real asynchronous operation that somehow has hold of this promise
callback and can call it when it completes. Also my promise Result types are purely artificial, but again you can see how they might be used to communicate something meaningful down the pipeline. When I say promise(.success(true))
, that causes true
to pop out the end of the pipeline; we are disregarding that here, but it could be instead a downright useful value of some sort, possibly even the next Future.
(Note also that we could insert .receive(on: DispatchQueue.main)
at any point in the chain to ensure that what follows immediately is started on the main thread.)
Slightly neater
It also occurs to me that we could make the syntax neater, perhaps a little closer to PromiseKit's lovely simple chain, by moving our Future publishers off into constants. If you do that, though, you should probably wrap them in Deferred publishers to prevent premature evaluation. So for example:
var storage = Set<AnyCancellable>()
func async1(_ promise:@escaping (Result<Bool,Error>) -> Void) {
delay(3) {
print("async1")
promise(.success(true))
}
}
func async2(_ promise:@escaping (Result<Bool,Error>) -> Void) {
delay(3) {
print("async2")
promise(.success(true))
}
}
override func viewDidLoad() {
print("start")
let f1 = Deferred{Future<Bool,Error> { promise in
self.async1(promise)
}}
let f2 = Deferred{Future<Bool,Error> { promise in
self.async2(promise)
}}
// this is now extremely neat-looking
f1.flatMap {_ in f2 }
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: {_ in}, receiveValue: {_ in print("done")})
.store(in:&self.storage) // storage is a persistent Set<AnyCancellable>
}
Chaining calls when using Future in Swift similar to PromiseKit
You can use a .flatMap
operator, which takes a value from upstream and produces a publisher. This would look something like below.
Note, that it's also better to return a type-erased AnyPublisher
at the function boundary, instead of the specific publisher used inside the function
func loginwithFacebook() -> AnyPublisher<UserProfileCompact, Error> {
loginWithFacebook().flatMap { authCredential in
loginWithFirebase(authCredential)
}
.eraseToAnyPublisher()
}
Chaining n requests in Combine
For this one, a lot depends on how you want to use the User objects. If you want them to all emit as individual users as they come in, then merge
is the solution. If you want to keep the array order and emit them all as an array of users once they all come in, then combineLatest
is what you need.
Since you are dealing with an array, and neither merge nor combineLatest have array versions, you will need to use reduce. Here's examples:
func combine(ids: [Int]) -> AnyPublisher<[User], Error> {
ids.reduce(Optional<AnyPublisher<[User], Error>>.none) { state, id in
guard let state = state else { return fetchUser(for: id).map { [$0] }.eraseToAnyPublisher() }
return state.combineLatest(fetchUser(for: id))
.map { $0.0 + [$0.1] }
.eraseToAnyPublisher()
}
?? Just([]).setFailureType(to: Error.self).eraseToAnyPublisher()
}
func merge(ids: [Int]) -> AnyPublisher<User, Error> {
ids.reduce(Optional<AnyPublisher<User, Error>>.none) { state, id in
guard let state = state else { return fetchUser(for: id).eraseToAnyPublisher() }
return state.merge(with: fetchUser(for: id))
.eraseToAnyPublisher()
}
?? Empty().eraseToAnyPublisher()
}
Notice that in the combine case, if the array is empty, the Publisher will emit an empty array then complete. In the merge case, it will just complete without emitting anything.
Also notice that in either case if any of the Publishers fail, then the entire chain will shut down. If you don't want that, you will have to catch the errors and do something with them...
PromiseKit 6 iOS chaining
The problem is because you're chaining off of a done
, which doesn't like that you're trying to then do a call to then
off of that.
Instead, you'll want to save the promise and use it for the later calls. You can do something like this:
let promise = firstly {
stellar.promiseFetchPayments(for: "")
}
promise.done { records in
print(records)
}
promise.then {
// call some other method
}.done { data in
// more data
}.catch { error in
print(error)
}
You can even return that promise from a method to use in other places, or pass it around to another method.
Setting backpressure in OperationQueue (or alternative API, e.g. PromiseKit, Combine Framework)
If you're using Combine, flatMap
can provide the back pressure. FlatMap
creates a publisher for each value it receives, but exerts back pressure when it reaches the specified maximum number of publishers that haven't completed.
Here's a simplified example. Assuming you have the following functions:
func loadImage(url: URL) -> AnyPublisher<UIImage, Error> {
// ...
}
func doImageProcessing(image: UIImage) -> AnyPublisher<Void, Error> {
// ...
}
let urls: [URL] = [...] // many image URLs
let processing = urls.publisher
.flatMap(maxPublishers: .max(5)) { url in
loadImage(url: url)
.flatMap { uiImage in
doImageProcessing(image: uiImage)
}
}
In the example above, it will load 5 images, and start processing them. The 6th image will start loading when one of the earlier ones is done processing.
Combine uncollect operator?
You can use flatMap
together with a new publisher
[1,2,3,4,5,6].publisher
.collect()
.flatMap { $0.publisher }
.sink { print($0) }
1
2
3
4
5
6
Combine using optional struct variable
You can use the publisher
of Optional
, which gives you a publisher that publishes one element if the optional is not nil, and an empty publisher otherwise.
You can then replaceEmpty(with: Data())
, and map
to an EventModel
.
func loadEvent(_ event: SCEvent) -> AnyPublisher<EventModel, Error> {
event.imagePath.publisher
.flatMap(DataDownloader.downloadData(fromPath:))
.replaceEmpty(with: Data())
.map {
EventModel(name: event.name, imageData: $0)
}
.eraseToAnyPublisher()
}
However, I don't think replacing with a Data()
is a good idea. A better design would be to replace with nil
, in which case you'll have to map to an optional first:
struct EventModel {
let name: String
let imageData: Data?
}
...
.flatMap(DataDownloader.downloadData(fromPath:))
.map { $0 as Data? } // here
.replaceEmpty(with: nil)
Related Topics
How to Check Is a String or Number
Swift: Extending Functionality of Print() Function
Swift 3 Error:Argument Labels '(_:)' Do Not Match Any Available Overloads
Does Swift Implement Tail Call Optimization? and in Mutual Recursion Case
Uploading Image to Firebase Storage and Database
Alamofire - Nsurlcache Is Not Working
How to Get User Home Directory Path (Users/"User Name") Without Knowing the Username in Swift3
How to Reason When I Have to Choose Between a Class, Struct and Enum in Swift
Interface Builder, @Iboutlet and Protocols for Delegate and Datasource in Swift
Ambiguous Reference to Member 'Tableview'
Replacing Calayer and Cabasicanimation with Skscene and Skactions
Firebase Instanceid.Instanceid().Token() Method Is Deprecated