PromiseKit

2018/9/6 posted in  iOS

then and done

一个典型的链条⛓️

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

如果这段代码使用之前的方式可能会是这样

login { creds, error in
    if let creds = creds {
        fetch(avatar: creds.user) { image, error in
            if let image = image {
                self.imageView = image
            }
        }
    }
}

其中重要的语法有then 是另一种方式的回调方法,但是承诺链比较容易理解,一个异步操作之后接着去执行下一个操作。

done是一样的,但是done不返回promise,它通常是链条成功部分的终结。

还有就是错误捕捉,比如我们以前的方法会有成功和错误的两个回调,但是现在使用了promise只需要返回一个promise就行了。如下所示

func login() -> Promise<Creds>
    
// Compared with:

func login(completion: (Creds?, Error?) -> Void)
                        // ^^ ugh. Optionals. Double optionals.

catch

那么错误是怎么捕捉到的呢,那么就需要用到promise的另一个重要的方法catch

firstly {
    login()
}.then { creds in
    fetch(avatar: creds.user)
}.done { image in
    self.imageView = image
}.catch {
    // any errors in the whole chain land here
}

每个promise都代表一个异步任务的对象,如果任务失败,诺言会失败,执行reject状态,如果诺言执行成功就是fulfilled状态,如果执行失败会跳过后面的所有then操作进入到catch当中。

ensure and finally

有时候我们需要执行一些必须要执行的操作,比如无论网络请求失败还是成功都隐藏请求动画,那么就需要用到ensure语句了。
无论你的promise执行结果如何,ensure当中的代码块都将被执行。

firstly {
    UIApplication.shared.isNetworkActivityIndicatorVisible = true
    return login()
}.then {
    fetch(avatar: $0.user)
}.done {
    self.imageView = $0
}.ensure {
    UIApplication.shared.isNetworkActivityIndicatorVisible = false
}.catch {
    //…
}

另外你也可以使用finally方法在链条的最底部执行,无须返回任何值。

spinner(visible: true)

firstly {
    foo()
}.done {
    //…
}.catch {
    //…
}.finally {
    self.spinner(visible: false)
}

when

使用完成处理程序,对多个异步操作的响应要么很慢,要么很难。慢意味着连续执行:

when 有点像GCD当中的DispatchGroup 做完之后再通知到接下来的操作。

firstly {
    when(fulfilled: operation1(), operation2())
}.done { result1, result2 in
    //…
}

when 接受承诺,等待他们解决并返回包含结果的承诺。

与任何承诺链一样,如果任何组件承诺失败,链将调用下一个catch

Making Promises

那么如何使自己的项目支持promise?
标准扩展程序将带您走很长的路,但有时您仍然需要启动自己的链。也许您正在使用不提供承诺的第三方API,或者您可能编写了自己的异步系统。无论哪种方式,添加承诺都很容易。如果您查看标准扩展的代码,您会看到它使用下面描述的相同方法。

假设我们有以下方法:

func fetch(completion: (String?, Error?) -> Void){

    }
我们可以使用下面方法变成promise
func fetch() -> Promise<String> {
        return Promise{
            seal in
            fetch(completion: seal.resolve)
        }

//      或者  return Promise { fetch(completion: $0.resolve) }
}

或者    
func fetch() -> Promise<String> {
        return Promise<String>{ seal in
            fetch{ (result, error) in
                seal.resolve(result, error)
            }
        }
}

Promise的初始化程序为您提供的seal对象定义了许多处理平常的完成处理程序的方法。它甚至涵盖了各种罕见的情况,因此您可以轻松地将promises添加到现有代码库中。

注意:在PMK 4中,此初始化程序为闭包提供了两个参数: fulfill和reject。PMK 5和6为您提供了具有两种方法fulfill和 reject方法的对象,但也提供了该方法的许多变体resolve。您通常只需将完成处理程序参数传递给,resolve并让Swift找出适用于您的特定情况的变体(如上例所示)。

还可以使用fulfill 和 reject方法,比如上面的代码可以改成下面的样子。

 func fetch() -> Promise<String> {

        return Promise<String>{ seal in
            
            fetch{ (result, error) in
                if let error = error {
                    seal.reject(error)
                }else if let success = result{
                    seal.fulfill(success)
                }else{
                    seal.reject(PMKError.invalidCallingConvention)
                }
            }
        }
    }

Guarantee

自从PromiseKit 5,提供了Guarantee作为补充 Promise。这样做是为了补充Swift强大的错误处理系统。

Guarantee永不失败,所以不能拒绝。一个很好的例子是after:

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

APIs That Use Promises

class MyRestAPI {
    func user() -> Promise<User> {
        return firstly {
            URLSession.shared.dataTask(.promise, with: url)
        }.compactMap {
            try JSONSerialization.jsonObject(with: $0.data) as? [String: Any]
        }.map { dict in
            User(dict: dict)
        }
    }

    func avatar() -> Promise<UIImage> {
        return user().then { user in
            URLSession.shared.dataTask(.promise, with: user.imageUrl)
        }.compactMap {
            UIImage(data: $0.data)
        }
    }
}

通过这种方式,异步链可以干净地无缝地整合来自整个应用程序的代码,而不会违反架构边界。

Background Work

class MyRestAPI {
    func avatar() -> Promise<UIImage> {
        let bgq = DispatchQueue.global(qos: .userInitiated)

        return firstly {
            user()
        }.then(on: bgq) { user in
            URLSession.shared.dataTask(.promise, with: user.imageUrl)
        }.compactMap(on: bgq) {
            UIImage(data: $0)
        }
    }
}

所有PromiseKit处理程序都带有一个on参数,允许您指定运行处理程序的调度队列。默认值始终是主队列。

PromiseKit 完全是线程安全的。

Failing Chains

如果错误发生在链条中间,只需抛出一个错误。

firstly {
    foo()
}.then { baz in
    bar(baz)
}.then { result in
    guard !result.isBad else { throw MyError.myIssue }
    //…
    return doOtherThing()
}

错误将在下一个catch处理程序中显示。

由于promises处理抛出的错误,除非你真的想在本地处理错误,否则你不必将调用函数包装到块中:

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

抽象异步性 Abstracting Away Asynchronicity

var fetch = API.fetch()

override func viewDidAppear() {
    fetch.then { items in
        //…
    }
}

func buttonPressed() {
    fetch.then { items in
        //…
    }
}

func refresh() -> Promise {
    // ensure only one fetch operation happens at a time

    if fetch.isResolved {
        startSpinner()
        fetch = API.fetch().ensure {
            stopSpinner()
        }
    }
    return fetch
}

使用promises,您无需担心异步操作何时完成。就像现在一样。

在上面,我们看到您可以根据自己的喜好多次调用then。所有块将按添加顺序执行。

Minimum Duration

有时您需要一项任务至少花费一定的时间。(例如,您希望显示进度条,但如果它显示的时间少于0.3秒,则UI对用户显示为断开。)

let waitAtLeast = after(seconds: 0.3)

firstly {
    foo()
}.then {
    waitAtLeast
}.done {
    //…
}

上面的代码有效,因为我们在工作之前创建了延迟foo()。当我们等待这个承诺时,要么它已经超时,要么我们将等待0.3秒的任何剩余时间,然后继续链。

Cancellation

Promise没有cancel函数,但它们通过符合CancellableError协议的特殊错误类型支持取消。

func foo() -> (Promise<Void>, cancel: () -> Void) {
    let task = Task(…)
    var cancelme = false

    let promise = Promise<Void> { seal in
        task.completion = { value in
            guard !cancelme else { return reject(PMKError.cancelled) }
            seal.fulfill(value)
        }
        task.start()
    }

    let cancel = {
        cancelme = true
        task.cancel()
    }

    return (promise, cancel)
}

被取消的链条默认不会进入到catch方法,然后也可以接收取消中断,如下

foo.then {
    //…
}.catch(policy: .allErrors) {
    // cancelled errors are handled *as well*
}

Saving Previous Results

举个例子

login().then { username in
    fetch(avatar: username)
}.done { image in
    //…
}

如果想在done在同时访问username和image呢
可以使用下面的方法

login().then { username in
    fetch(avatar: username).done { image in
        // we have access to both `image` and `username`
    }
}.done {
    // the chain still continues as you'd expect
}

然而,这种嵌套降低了链条的清晰度。相反,我们可以使用Swift元组:

//简化版
login().then { username in
    fetch(avatar: username).map { ($0, username) }
}.then { image, username in
    //…
}

Chaining Sequences

当你在一个数组中有一系列的任务要反应的时候

// fade all visible table cells one by one in a “cascading” effect

var fade = Guarantee.value(Bool())

for cell in tableView.visibleCells {

    fade = fade.then {_ in
        UIView.animate(.promise, duration: 1, animations: {
            cell.alpha = 0

        })

    }
}
fade.done {_ in


}
        

或者你有一个包含promise的数组

var foo = Promise()
for nextPromise in arrayOfPromises {
    foo = foo.then { nextPromise }
}
foo.done {
    // finish
}

之前我们知道可以使用when同时异步执行多个promise,这种方式完成的速度更快,但是有时候我们需要promise顺序一个一个的执行,比如上面的动画。

Promise采用关联对象进行保存对象

oc中运行时关联对象 宏定义写法

extern void *PMKManualReferenceAssociatedObject;
#define PMKRetain(obj)  objc_setAssociatedObject(obj, PMKManualReferenceAssociatedObject, obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
#define PMKRelease(obj) objc_setAssociatedObject(obj, PMKManualReferenceAssociatedObject, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC)