2

I am setting up an app that utilizes promiseKit as a way to order asynchronous tasks. I currently have a set up which ensures two async functions (referred to as promises) are done in order (lets call them 1 and 2) and that another set of functions (3 and 4) are done in order. Roughly:

import PromiseKit
override func viewDidAppear(_ animated: Bool) {


        firstly{
            self.promiseOne() //promise #1 happening first (in relation to # 1 and #2)
            }.then{_ -> Promise<[String]> in
                self.promiseTwo()//promise #2 starting after 1 has completed
            }
            .catch{ error in
                print(error)
        }
        firstly{
            self.promiseThree()//Promise #3 happening first (in relation to #3 and #4)
            }.then{_ -> Promise<[String]> in
                self.promiseFour()//Promise #4 starting after #3 has completed
            }.
            .catch{ error in
                print(error)
        }
}

Each firstly ensures the order of the functions within them by making sure the first one is completed before the second one can initiate. Using two separate firstly's ensures that 1 is done before 2, 3 is done before 4, and (importantly) 1 and 3 start roughly around the same time (at the onset of the viewDidAppear()). This is done on purpose because 1 and 3 are not related to each other and can start at the same time without any issues (same goes for 2 and 4). The issue is that there is a fifth promise, lets call it promiseFive that must only be run after 2 and 4 have been completed. I could just link one firstly that ensure the order is 1,2,3,4,5, but since the order of 1/2 and 3/4 is not relevant, linking them in this fashion would waste time. I am not sure how to set this up so that promiseFive is only run upon completion of both 2 and 4. I have thought to have boolean-checked functions calls at the end of both 2 and 4, making sure the other firstly has finished to then call promiseFive but, since they begin asynchronously (1/2 and 3/4), it is possible that promiseFive would be called by both at the exact same time with this approach, which would obviously create issues. What is the best way to go about this?

Runeaway3
  • 1,439
  • 1
  • 17
  • 43
  • 1
    Do some [searching on `DispatchGroup`](https://stackoverflow.com/search?q=%5Bswift%5D+is%3Aa+DispatchGroup). – rmaddy Jul 17 '17 at 23:24
  • @rmaddy - If he was just splashing around in the shallow end of the GCD pool and asynchronous APIs, then groups are a great way to tackle this. But given that he's using promises, it would be a mistake to introduce groups in this environment, IMHO... – Rob Jul 18 '17 at 01:30
  • @Rob I'm not familiar with promise kit. And honestly I glossed over that when I posted my comment. – rmaddy Jul 18 '17 at 01:32

2 Answers2

2

You can use when or join to start something after multiple other promises have completed. The difference is in how they handled failed promises. It sounds like you want join. Here is a concrete, though simple example.

This first block of code is an example of how to create 2 promise chains and then wait for both of them to complete before starting the next task. The actual work being done is abstracted away into some functions. Focus on this block of code as it contains all the conceptual information you need.

Snippet

let chain1 = firstly(execute: { () -> (Promise<String>, Promise<String>) in
    let secondPieceOfInformation = "otherInfo" // This static data is for demonstration only

    // Pass 2 promises, now the next `then` block will be called when both are fulfilled
    // Promise initialized with values are already fulfilled, so the effect is identical
    // to just returning the single promise, you can do a tuple of up to 5 promises/values
    return (fetchUserData(), Promise(value: secondPieceOfInformation))

}).then { (result: String, secondResult: String) -> Promise<String> in
    self.fetchUpdatedUserImage()
}

let chain2 = firstly {
    fetchNewsFeed() //This promise returns an array

}.then { (result: [String : Any]) -> Promise<String> in

    for (key, value) in result {
        print("\(key) \(value)")
    }
    // now `result` is a collection
    return self.fetchFeedItemHeroImages()
}

join(chain1, chain2).always {
    // You can use `always` if you don't care about the earlier values

    let methodFinish = Date()
    let executionTime = methodFinish.timeIntervalSince(self.methodStart)
    print(String(format: "All promises finished %.2f seconds later", executionTime))
}

PromiseKit uses closures to provide it's API. Closures have an scope just like an if statement. If you define a value within an if statement's scope, then you won't be able to access it outside of that scope.

You have several options to passing multiple pieces of data to the next then block.

  1. Use a variable that shares a scope with all of the promises (you'll likely want to avoid this as it works against you in managing the flow of asynchronous data propagation)
  2. Use a custom data type to hold both (or more) values. This can be a tuple, struct, class, or enum.
  3. Use a collection (such as a dictionary), example in chain2
  4. Return a tuple of promises, example included in chain1

You'll need to use your best judgement when choosing your method.

Complete Code

import UIKit
import PromiseKit

class ViewController: UIViewController {

    let methodStart = Date()

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        <<Insert The Other Code Snippet Here To Complete The Code>>

        // I'll also mention that `join` is being deprecated in PromiseKit
        // It provides `when(resolved:)`, which acts just like `join` and
        // `when(fulfilled:)` which fails as soon as any of the promises fail
        when(resolved: chain1, chain2).then { (results) -> Promise<String> in
            for case .fulfilled(let value) in results {
                // These promises succeeded, and the values will be what is return from
                // the last promises in chain1 and chain2
                print("Promise value is: \(value)")
            }

            for case .rejected(let error) in results {
                // These promises failed
                print("Promise value is: \(error)")
            }

            return Promise(value: "finished")
            }.catch { error in
                // With the caveat that `when` never rejects
        }
    }

    func fetchUserData() -> Promise<String> {
        let promise = Promise<String> { (fulfill, reject) in

            // These dispatch queue delays are standins for your long-running asynchronous tasks
            // They might be network calls, or batch file processing, etc
            // So, they're just here to provide a concise, illustrative, working example
            DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) {
                let methodFinish = Date()
                let executionTime = methodFinish.timeIntervalSince(self.methodStart)

                print(String(format: "promise1 %.2f seconds later", executionTime))
                fulfill("promise1")
            }
        }

        return promise
    }

    func fetchUpdatedUserImage() -> Promise<String> {
        let promise = Promise<String> { (fulfill, reject) in
            DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) {
                let methodFinish = Date()
                let executionTime = methodFinish.timeIntervalSince(self.methodStart)

                print(String(format: "promise2 %.2f seconds later", executionTime))
                fulfill("promise2")
            }
        }

        return promise
    }

    func fetchNewsFeed() -> Promise<[String : Any]> {
        let promise = Promise<[String : Any]> { (fulfill, reject) in
            DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
                let methodFinish = Date()
                let executionTime = methodFinish.timeIntervalSince(self.methodStart)

                print(String(format: "promise3 %.2f seconds later", executionTime))
                fulfill(["key1" : Date(),
                         "array" : ["my", "array"]])
            }
        }

        return promise
    }

    func fetchFeedItemHeroImages() -> Promise<String> {
        let promise = Promise<String> { (fulfill, reject) in
            DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) {
                let methodFinish = Date()
                let executionTime = methodFinish.timeIntervalSince(self.methodStart)

                print(String(format: "promise4 %.2f seconds later", executionTime))
                fulfill("promise4")
            }
        }

        return promise
    }
}

Output

promise3 1.05 seconds later
array ["my", "array"]
key1 2017-07-18 13:52:06 +0000
promise1 2.04 seconds later
promise4 3.22 seconds later
promise2 4.04 seconds later
All promises finished 4.04 seconds later
Promise value is: promise2
Promise value is: promise4

allenh
  • 6,582
  • 2
  • 24
  • 40
  • Is the `DispatchQueue.global().asyncAfter(deadline: .now() + 2.0)` necessary? If so, why? – Runeaway3 Jul 18 '17 at 06:15
  • Also, for some reason, I can't access variables I create outside the promise closures. I can pass one from the first part of promise1 to the next, but I need to pass 2 variables which it is not letting me do. Anyway around this? – Runeaway3 Jul 18 '17 at 06:44
  • @AlekPiasecki I updated my post to hopefully address your followup questions. There is a lot of information in my answer, please read carefully and actually run the code to get familiar with what it's doing. I also recommend perusing the PromiseKit source code as there is some good documentation in there. Let me know if you have any further follow up. – allenh Jul 18 '17 at 13:58
  • My question was very simplified and I actually use a three promise chain and a 6 promise chain in my app. I had some difficulty applying your answer (which was very good) directly to the promises I use. I managed to do a variation of it which led to a ~3.5 second delay between completion of the 1st and 2nd chains. This seems a bit high to me as using the method I illustrate in https://stackoverflow.com/questions/45202663/how-to-utilize-nslock-to-prevent-a-function-from-firing-twice yields only a ~0.3 second delay. Is there anyway we can discuss this more indepthly? – Runeaway3 Jul 19 '17 at 23:32
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/149657/discussion-between-allen-humphreys-and-alek-piasecki). – allenh Jul 20 '17 at 00:09
1

The details depend a little upon what types of these various promises are, but you can basically return the promise of 1 followed by 2 as one promise, and return the promise of 3 followed by 4 as another, and then use when to run those two sequences of promises concurrently with respect to each other, but still enjoy the consecutive behavior within those sequences. For example:

let firstTwo = promiseOne().then { something1 in
    self.promiseTwo(something1)
}

let secondTwo = promiseThree().then { something2 in
    self.promiseFour(something2)
}

when(fulfilled: [firstTwo, secondTwo]).then { results in
    os_log("all done: %@", results)
}.catch { error in
    os_log("some error: %@", error.localizedDescription)
}

This might be a situation in which your attempt to keep the question fairly generic might make it harder to see how to apply this answer in your case. So, if you are stumbling, you might want to be more specific about what these four promises are doing and what they're passing to each other (because this passing of results from one to another is one of the elegant features of promises).

Rob
  • 415,655
  • 72
  • 787
  • 1,044