background

I have known that there is such a thing as Promise in JS, which can implement asynchronous methods in a friendly way. Later, I happened to see this usage in an ios open source code:

firstly {
    login()
}.then { creds in
    fetch(avatar: creds.user)
}.done { image in
    self.imageView = image
}
Copy the code

Firstly, XXX is the first step, then XXX is the next step, and XXX is the last step after done. This writing method is really swift, which immediately aroused my interest. Although I also have a scheme to realize asynchronous callback, there are some obscure knowledge to understand, such as cold signal and hot signal. The most interesting thing is its syntax. To write a simple logic, I need to create a variety of new producers. Methods called by cutting threads are always confused between subscribeOn and observeOn, and different positions also affect the execution order. Anyway, the world is much better now that we’ve seen the Promise syntax, so let’s get into the Promise world.

PromiseKit

then & done

The Promise object is a SignalProducer in ReactCocoa, which can asynchronously fullfill returns a success object or Reject returns an error signal.

Promise { sink in
    it.requestJson().on(failed: { err in
        sink.reject(err)
    }, value: { data in
        sink.fulfill(data)
    }).start()
}
Copy the code

The next step is to use it in each method block, for example:

firstly {
    Promise { sink in
        indicator.show(inView: view, text: text, detailText: nil, animated: true)
        sink.fulfill()
    }
}.then {
        api.promise(format: .json)
}.ensure {
        indicator.hide(inView: view, animated: true)
}.done { data in
        letparams = data.result! ["args"] as! [String: String]
        assert((Constant.baseParams + Constant.params) == params)
}.catch { error in
        assertionFailure()}Copy the code

Firstly is optional and can only be placed first in order to make the code more elegant and tidy. It also returns a Promise in its block. “Then” is connected in the middle, and infinitely many “THEN” can be connected to each other, as the name suggests, just as we can tell stories with “then, then, then…” Then also requires the return of a Promise object, that is, any THEN can throw an error, interrupt event. Unlike ensure, which is similar to Finally in that it will be executed regardless of whether an event is wrong or not, ensure can be placed anywhere. Done indicates the end of an event. It must be executed only when all the preceding events are successfully executed. Catch is to catch an exception, and any event that fails before done goes directly to the catch.

Loading is displayed first, and then the API is requested. Ensure that loading is hidden regardless of whether the API is successfully requested. If loading succeeds, the data is printed; otherwise, the printing is abnormal.

Guarantee

Guarantee is the special case of a Promise. We can use Guarantee instead of a Promise when we are sure that the event will not be wrong, so we don’t need a catch to catch exceptions:

firstly {
    after(seconds: 0.1)
}.done {
    // there is no way to add a `catch` because after cannot fail.
}
Copy the code

After is a lazy method that returns a Guarantee object, and since lazy execution is guaranteed not to fail, we just need to follow up with done.

map

A map is a transformation of data, not an event. For example, if we want to convert JSON data returned from an interface into an object, we can use a map, which also returns an object, not a Promise.

tap

Tap is a non-intrusive event, similar to Reactivecocoa’s doNext, which does not affect any properties of the event, but does something that does not affect the main line when appropriate.

firstly {
    foo()
}.tap {
    print($0)
}.done {
    / /...
}.catch {
    / /...
}
Copy the code

when

When is a good option for performing multiple tasks in parallel. When allows events to proceed to the next stage when all events complete, or when any event fails. In addition, when has a concurrently property that controls the maximum number of concurrent tasks:

firstly {
    Promise { sink in
        indicator.show(inView: view, text: text, detailText: nil, animated: true)
        sink.fulfill()
    }
}.then {
        when(fulfilled: api.promise(format: .json), api2.promise(format: .json))
}.ensure {
        indicator.hide(inView: view, animated: true)
}.done { data, data2 in
        assertionFailure()
        expectation.fulfill()
}.catch { error in
        assert((error as! APError).description == err.description)
        expectation.fulfill()
}
Copy the code

This method is commonly used when we need to wait for data from two or three interfaces to be obtained at the same time before doing subsequent work.

on

PromiseKit switching threads is very convenient and intuitive, just pass in the thread of ON in the method:

firstly {
    user()
}.then(on: DispatchQueue.global()) { user in
    URLSession.shared.dataTask(.promise, with: user.imageUrl)
}.compactMap(on: DispatchQueue.global()) {
    UIImage(data: $0)}Copy the code

Whichever method needs to specify the thread is passed to the corresponding thread on that method.

throw

If you need to throw an exception in then, one way is to call reject in a Promise, and another simpler way is to throw directly:

firstly {
    foo()
}.then { baz in
    bar(baz)
}.then { result in
    guard! result.isBadelse { throw MyError.myIssue }
    / /...
    return doOtherThing()
}
Copy the code

If the called method might throw an exception, the try will also direct the exception to a catch:

foo().then { baz in
    bar(baz)
}.then { result in
    try doOtherThing()
}.catch { error in
    // if doOtherThing() throws, we end up here
}
Copy the code

recover

CLLocationManager.requestLocation().recover { error -> Promise<CLLocation> in
    guard error == MyError.airplaneMode else {
        throw error
    }
    return .value(CLLocation.savannah)
}.done { location in
    / /...
}
Copy the code

Recover saves the task from exceptions, allowing certain errors to be ignored and returned as normal, while remaining errors continue to throw exceptions.

A few examples

The order of each line of the list gradually disappears

let fade = Guarantee(a)for cell in tableView.visibleCells {
    fade = fade.then {
        UIView.animate(.promise, duration: 0.1) {
            cell.alpha = 0
        }
    }
}
fade.done {
    // finish
}
Copy the code

Executes a method that specifies a timeout

let fetches: [Promise<T>] = makeFetches()
let timeout = after(seconds: 4)

race(when(fulfilled: fetches).asVoid(), timeout).then {
    / /...
}

Copy the code

Race requires that all tasks return the same type. It is best to return Void for all tasks. In the example above, the 4-second timer and the request API are initiated at the same time. The subsequent methods are called directly.

At least wait a while to do something

let waitAtLeast = after(seconds: 0.3)

firstly {
    foo()
}.then {
    waitAtLeast
}.done {
    / /...
}
Copy the code

The example above starts before Foo in Firstly executes after(seconds: 0.3), so if foo executes more than 0.3 seconds, foo will not wait another 0.3 seconds and will move on to the next task. If Foo executes for less than 0.3 seconds, it will wait until 0.3 seconds before continuing. This method of scene can be used in the startup page animation, animation display requires a guarantee time.

retry

func attempt<T>(maximumRetryCount: Int = 3, delayBeforeRetry: DispatchTimeInterval = .seconds(2)._ body: @escaping () -> Promise<T- > >)Promise<T> {
    var attempts = 0
    func attempt(a) -> Promise<T> {
        attempts += 1
        return body().recover { error -> Promise<T> in
            guard attempts < maximumRetryCount else { throw error }
            return after(delayBeforeRetry).then(on: nil, attempt)
        }
    }
    return attempt()
}

attempt(maximumRetryCount: 3) {
    flakeyTask(parameters: foo)
}.then {
    / /...
}.catch { _ in
    // we attempted three times but still failed
}
Copy the code

Delegate change Promise

extension CLLocationManager {
    static func promise(a) -> Promise<CLLocation> {
        return PMKCLLocationManagerProxy().promise
    }
}

class PMKCLLocationManagerProxy: NSObject.CLLocationManagerDelegate {
    private let (promise, seal) = Promise"[CLLocation]>.pending()
    private var retainCycle: PMKCLLocationManagerProxy?
    private let manager = CLLocationManager(a)init() {
        super.init()
        retainCycle = self
        manager.delegate = self // does not retain hence the `retainCycle` property

        promise.ensure {
            // ensure we break the retain cycle
            self.retainCycle = nil}}@objc fileprivate func locationManager(_: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        seal.fulfill(locations)
    }

    @objc func locationManager(_: CLLocationManager, didFailWithError error: Error) {
        seal.reject(error)
    }
}

// use:

CLLocationManager.promise().then { locations in
    / /...
}.catch { error in
    / /...
}
Copy the code

RetainCycle is one of the circular reference, the purpose is to prevent PMKCLLocationManagerProxy itself is released, when the end of the Promise, performs the self in ensure method. The reference to remove retainCycle = nil, To achieve the goal of releasing oneself. Very clever.

Passing intermediate results

Sometimes we need to pass intermediate results from a task, such as the following example where the done variable cannot be used:

login().then { username in
    fetch(avatar: username)
}.done { image in
    / /...
}
Copy the code

The result can be returned as a tuple in a clever way using map:

login().then { username in
    fetch(avatar: username).map{($0, username) }
}.then { image, username in
    / /...
}
Copy the code

conclusion

Although PromiseKit is similar to Reactivecocoa in many uses and principles, its simplicity and intuitionality are the most attractive features of PromiseKit