18

I was using PromiseKit successfully in a project until Xcode 11 betas broke PK v7. In an effort to reduce external dependencies, I decided to scrap PromiseKit. The best replacement for handling chained async code seemed to be Futures using the new Combine framework.

I am struggling to replicate the simple PK syntax using Combine

ex. simple PromiseKit chained async call syntax

getAccessCodeFromSyncProvider.then{accessCode in startSync(accessCode)}.then{popToRootViewController}.catch{handleError(error)}

I understand:

A Swift standard library implementation of async/await would solve this problem (async/await does not yet exist, despite lots of chatter and involvement from Chris Latter himself)

I could replicate using Semaphores (error-prone?)

flatMap can be used to chain Futures

The async code I'd like should be able to be called on demand, since it's involved with ensuring user is logged in. I'm wrestling with two conceptual problems.

  1. If I wrap Futures in a method, with sink to handle result, it seems that the method goes out of scope before subscriber is called by sink.

  2. Since Futures execute only once, I worry that if I call the method multiple times I'll only get the old, stale, result from the first call. To work around this, maybe I would use a PassthroughSubject? This allows the Publisher to be called on demand.

Questions:

  1. Do I have to retain every publisher and subscriber outside of the calling method
  2. How can I replicate simple chained async using the Swift standard library and then embed this in a swift instance method I can call on-demand to restart the chained async calls from the top??
//how is this done using Combine?
func startSync() {
 getAccessCodeFromSyncProvider.then{accessCode in startSync(accessCode)}.catch{\\handle error here}
}
Small Talk
  • 747
  • 1
  • 6
  • 15
  • Extremely broad and wide ranging. Can you focus down the question? At the very least explain the goal of your code. Assume we don’t know what any of your methods do. You say “how is this done” but what is “this”? – matt Dec 20 '19 at 18:33
  • Matt, love your books!! They were critical when I was first learning.I will try to simplify the question with specific code flow. As a first cut, I would say I'm trying to implement the simplest form of async/await in Swift, at the highest level of abstraction, without depending on 3rd party libraries like PromiseKit. PK has wonderful syntax that I'd like to replicate. My code using PK would read somewhat like 'firstly{async}.then{async}.recover{async}.done{clean-up}.catch{handle errors}. Self-documenting, and easy to reason about. That's my aim, only using Swift standard library. – Small Talk Dec 20 '19 at 19:25
  • Aha! Well, Combine is not PromiseKit, I'm afraid. You can chain async things for sure, but it won't be the same. – matt Dec 20 '19 at 19:41

4 Answers4

26

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>
}
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Thanks Matt!, I'm still a bit hazy on role of .map, vs. .flatMap, but I'll study up. The only thing I don't see is .receiveOn. (I've had weird bugs happen when .receiveCompletion is not done on main thread.) But I'll test the above in a playground. Thanks for giving me a running start. Knowing a persistent ref to final subscriber is mandatory is helpful. (I think my first effort failed because the subscriber was auto-canceled when calling method in which it was contained went out of scope before the publisher finished its async call.) Let me play before ✔. – Small Talk Dec 20 '19 at 21:15
  • 1
    Receive(on) is so easy I didn't put it in, and it wasn't needed for this example. I'll add it if you like. – matt Dec 20 '19 at 22:08
  • Also I didn't bother to catch errors because there are _so_ many ways to deal with errors that there's no point prejudicing the matter. My `sink` does in fact catch errors but it ignores them at the moment. But you could instead map an error, catch and emit something that isn't an error, etc. etc. – matt Dec 20 '19 at 22:12
  • Hi Matt, noted. Everything adds up. I'm going to mark this as accepted as as was able to get it to work in SwiftUI. I extracted the code in viewDidLoad into a separate class and instance method. I called the method in SwiftUI (in an .onAppear modifier). I verified that each time the method was called, it would fire-off a new set of discrete async calls. So this is a 'poor man's PromiseKit'. Downside is that it lacks elegant syntax, upside is no dependencies and uses a standard Combine publishers > subscriber flow, with Futures to section off chunks of async code. – Small Talk Dec 20 '19 at 22:45
  • 1
    By the way I just eliminated the use of `map`, silly me, you can just `flatMap` directly on to the next publisher. Facepalm. – matt Dec 20 '19 at 22:46
  • Arms-length plug: Look up Matt's books on iOS development. Whenever I got truly stuck, especially dealing with views, his books got me unstuck. An unexpected pleasure to receive help from such an elite source. – Small Talk Dec 20 '19 at 22:52
  • 1
    Nothing elite about it, this is how I learn stuff! — I've added another section to my answer showing how we can neaten up the actual expression of the pipeline by moving the Future publishers into local constants. Combine will never be PromiseKit, as you have rightly said, but it's probably the closest we've got without resorting to third-party libraries, and I think you were right to start down this road. – matt Dec 20 '19 at 22:55
  • Thanks Matt, was able to just now strip all old PK code and replace with approach suggested above. App compiles. I'm back in business. – Small Talk Dec 21 '19 at 02:46
  • Hi @matt great answer! Can you helm me with little more difficult case - I have Array of objects and bunch of processors with interface AnyPublisher I need process all element one by one. In my approach I use items.publisher and then chain everything via flatMap, but all items start processing near immediately... How to chain in my case? – greenhost87 Dec 01 '20 at 20:41
  • @greenhost87 in flatMap call, set maxpublishers parameter to .max(1)? See whole discussion at https://stackoverflow.com/questions/59743938/combine-framework-serialize-async-operations – matt Dec 01 '20 at 22:22
11

matt's answer is correct, use flatMap to chain promises. I got in the habit of returning promises when using PromiseKit, and carried it over to Combine (returning Futures).

I find it makes the code easier to read. Here's matt's last example with that recommendation:

var storage = Set<AnyCancellable>()

func async1() -> Future<Bool, Error> {
  Future { promise in
    delay(3) {
      print("async1")
      promise(.success(true))
    }
  }
}

func async2() -> Future<Bool, Error> {
  Future { promise in
    delay(3) {
      print("async2")
      promise(.success(true))
    }
  }
}

override func viewDidLoad() {
  print("start")

  async1()
    .flatMap { _ in async2() }
    .receive(on: DispatchQueue.main)
    .sink(receiveCompletion: {_ in}, receiveValue: {_ in print("done")})
    .store(in:&self.storage) // storage is a persistent Set<AnyCancellable>
}

Note that AnyPublisher will work as a return value as well, so you could abstract away the Future and have it return AnyPublisher<Bool, Error> instead:

func async2() -> AnyPublisher<Bool, Error> {
  Future { promise in
    delay(3) {
      print("async2")
      promise(.success(true))
    }
  }.eraseToAnyPubilsher()
}
Senseful
  • 86,719
  • 67
  • 308
  • 465
4

Also if you want to use the PromiseKit-like syntax, here are some extensions for Publisher

I am using this to seamlessly switch from PromiseKit to Combine in a project

extension Publisher {
    
    func then<T: Publisher>(_ closure: @escaping (Output) -> T) -> Publishers.FlatMap<T, Self>
    where T.Failure == Self.Failure {
        flatMap(closure)
    }
    
    func asVoid() -> Future<Void, Error> {
        return Future<Void, Error> { promise in
            let box = Box()
            let cancellable = self.sink { completion in
                if case .failure(let error) = completion {
                    promise(.failure(error))
                } else if case .finished = completion {
                    box.cancellable = nil
                }
            } receiveValue: { value in
                promise(.success(()))
            }
            box.cancellable = cancellable
        }
    }
    
    @discardableResult
    func done(_ handler: @escaping (Output) -> Void) -> Self {
        let box = Box()
        let cancellable = self.sink(receiveCompletion: {compl in
            if case .finished = compl {
                box.cancellable = nil
            }
        }, receiveValue: {
            handler($0)
        })
        box.cancellable = cancellable
        return self
    }
    
    @discardableResult
    func `catch`(_ handler: @escaping (Failure) -> Void) -> Self {
        let box = Box()
        let cancellable = self.sink(receiveCompletion: { compl in
            if case .failure(let failure) = compl {
                handler(failure)
            } else if case .finished = compl {
                box.cancellable = nil
            }
        }, receiveValue: { _ in })
        box.cancellable = cancellable
        return self
    }
    
    @discardableResult
    func finally(_ handler: @escaping () -> Void) -> Self {
        let box = Box()
        let cancellable = self.sink(receiveCompletion: { compl in
            if case .finished = compl {
                handler()
                box.cancellable = nil
            }
        }, receiveValue: { _ in })
        box.cancellable = cancellable
        return self
    }
}

fileprivate class Box {
    var cancellable: AnyCancellable?
}

And here's an example of use:

func someSync() {
    Future<Bool, Error> { promise in
        delay(3) {
            promise(.success(true))
        }
    }
    .then { result in
        Future<String, Error> { promise in
            promise(.success("111"))
        }
    }
    .done { string in
        print(string)
    }
    .catch { err in
        print(err.localizedDescription)
    }
    .finally {
        print("Finished chain")
    }
}
  • 1
    Just starting to explore PromiseKit, comparing to Combine, and wondering if there is value in adopting any of the non-Apple frameworks — or, just use Combine. There are clearly differences but I’m curious... with the above + what’s coming (very soon) for Combine and Swift 5.5 (async, actors)... does PromiseKit offer much advantage? (I’m vaguely aware there are quite a few PK extensions that offer iOS specific features but... unfamiliar with them so wondering what your opinion might be). I noticed your wrote “seamlessly switch from PK to Combine.” Seems to indicate you are dropping PK. – Zaphod May 24 '21 at 02:16
  • PromiseKit has several advantages over Future in Combine. For example, for each promise, you can highlight the value or error through the enumerated property "value". Combine will have to write an extension. But the main advantage of Combine is that it is a native framework and you no longer need to inject third-party dependencies to simplify the asynchronous work in the application. – Артем Балашов May 25 '21 at 05:33
  • Also, for any iOS feature, it is quite possible to write an extension for Combine, via Future or Publisher, if a sequence of values ​​is required. – Артем Балашов May 25 '21 at 05:38
-7

You can use this framework for Swift coroutines, it's also can be used with Combine - https://github.com/belozierov/SwiftCoroutine

DispatchQueue.main.startCoroutine {
    let future: Future<Bool, Error>
    let coFuture = future.subscribeCoFuture()
    let bool = try coFuture.await()

}
Alex Belozierov
  • 131
  • 1
  • 4
  • 1
    Your profile indicates you're associated with the sites you have linked. Linking to something you're affiliated with (e.g. a library, tool, product, or website) **without disclosing it's yours** is considered spam on Stack Overflow. See: [What signifies "Good" self promotion?](//meta.stackexchange.com/q/182212), [some tips and advice about self-promotion](/help/promotion), [What is the exact definition of "spam" for Stack Overflow?](//meta.stackoverflow.com/q/260638), and [What makes something spam](//meta.stackexchange.com/a/58035). – Samuel Liew Jul 28 '20 at 08:55