Search code examples
swiftasynchronouspromisekit

How to ensure two asynchronous tasks that 'start' together are completed before running another?


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?


Solution

  • 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