以前就了解到js中有Promise这么一个东西,能够很友好的实现异步方法,后来偶然在一段ios开源代码中看到这么一段用法:ios
firstly {
login()
}.then { creds in
fetch(avatar: creds.user)
}.done { image in
self.imageView = image
}
复制代码
眼前一亮,firstly第一步作xxx,then接下来作xxx,done完成了以后最后作xxx,这个写法真是太swift了,顿时产生了兴趣。 虽然实现异步回调我也有ReactCocoa的方案,但其中不乏一些晦涩难懂的知识须要理解,例如冷信号与热信号,最让人吐槽的仍是它的语法,写一个简单的逻辑就须要new各类Producer,切线程调用的方法又总是分不清subscribeOn和observeOn,并且放的位置不一样还影响执行顺序。 总之,在看到Promise语法以后,世界变得美好多了,接下来咱们就进入Promise的世界吧。git
Promise对象就是一个ReactCocoa中的SignalProducer,它能够异步fullfill返回一个成功对象或者reject返回一个错误信号。github
Promise { sink in
it.requestJson().on(failed: { err in
sink.reject(err)
}, value: { data in
sink.fulfill(data)
}).start()
}
复制代码
接下来就是把它用在各个方法块里面了,例如:json
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
let params = data.result!["args"] as! [String: String]
assert((Constant.baseParams + Constant.params) == params)
}.catch { error in
assertionFailure()
}
复制代码
firstly是可选的,它只能放在第一个,是为了代码能更加的优雅和整齐,他的block里也是return一个Promise。 then是接在中间的,能够无限多个then相互链接,顾名思义,就像咱们讲故事能够不断地有而后、而后、而后...then也是要求返回一个Promise对象的,也就是说,任何一个then均可以抛出一个error,中断事件。 ensure相似于finally,无论事件是否错误,它都必定会获得执行,ensure不一样于finally的是,它能够放在任何位置。 done是事件结束的标志,它是必需要有的,只有上面的事件都执行成功时,才会最终执行done。 catch是捕获异常,done以前的任何事件出现错误,都会直接进入catch。swift
上面代码的含义就是先显示loading,而后请求api,无论api是否请求成功,都要确保loading隐藏,而后若是成功,则打印数据,不然打印异常。api
Guarantee是Promise的特殊状况,当咱们确保事件不会有错误的时候,就能够用Guarantee来代替Promise,有它就不须要catch来捕获异常了:promise
firstly {
after(seconds: 0.1)
}.done {
// there is no way to add a `catch` because after cannot fail.
}
复制代码
after是一个延迟执行的方法,它就返回了一个Guarantee对象,由于延迟执行是必定不会失败的,因此咱们只须要后续接done就好了。并发
map是指一次数据的变换,而不是一次事件,例如咱们要把从接口返回的json数据转换成对象,就能够用map,map返回的也是一个对象,而不是Promise。异步
tap是一个无侵入的事件,相似于Reactivecocoa的doNext,他不会影响事件的任何属性,只是在适当的时机作一些不影响主线的事情,适用于打点:ide
firstly {
foo()
}.tap {
print($0)
}.done {
//…
}.catch {
//…
}
复制代码
when是个能够并行执行多个任务的好东西,when中当全部事件都执行完成,或者有任何一个事件执行失败,都会让事件进入下一阶段,when还有一个concurrently属性,能够控制并发执行任务的最多数量:
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()
}
复制代码
这个方法仍是很经常使用的,当咱们要同时等2,3个接口的数据都拿到,再作后续的事情的时候,就适合用when了。
PromiseKit的切换线程很是的方便和直观,只须要在方法中传入on的线程便可:
firstly {
user()
}.then(on: DispatchQueue.global()) { user in
URLSession.shared.dataTask(.promise, with: user.imageUrl)
}.compactMap(on: DispatchQueue.global()) {
UIImage(data: $0)
}
复制代码
哪一个方法须要指定线程就在那个方法的on传入对应的线程。
若是then中须要抛出异常,一种方法是在Promise中调用reject,另外一种比较简便的方法就是直接throw:
firstly {
foo()
}.then { baz in
bar(baz)
}.then { result in
guard !result.isBad else { throw MyError.myIssue }
//…
return doOtherThing()
}
复制代码
若是调用的方法可能会抛出异常,try也会让异常直达catch:
foo().then { baz in
bar(baz)
}.then { result in
try doOtherThing()
}.catch { error in
// if doOtherThing() throws, we end up here
}
复制代码
CLLocationManager.requestLocation().recover { error -> Promise<CLLocation> in
guard error == MyError.airplaneMode else {
throw error
}
return .value(CLLocation.savannah)
}.done { location in
//…
}
复制代码
recover能从异常中拯救任务,能够断定某些错误就忽略,当作正常结果返回,剩下的错误继续抛出异常。
let fade = Guarantee()
for cell in tableView.visibleCells {
fade = fade.then {
UIView.animate(.promise, duration: 0.1) {
cell.alpha = 0
}
}
}
fade.done {
// finish
}
复制代码
let fetches: [Promise<T>] = makeFetches()
let timeout = after(seconds: 4)
race(when(fulfilled: fetches).asVoid(), timeout).then {
//…
}
复制代码
race和when不同,when会等待全部任务执行成功再继续,race是谁第一个到就继续,race要求全部任务返回类型必须同样,最好的作法是都返回Void,上面的例子就是让4秒计时和请求api同时发起,若是4秒计时到了请求还没回来,则直接调用后续方法。
let waitAtLeast = after(seconds: 0.3)
firstly {
foo()
}.then {
waitAtLeast
}.done {
//…
}
复制代码
上面的例子从firstly中的foo执行以前就已经开始after(seconds: 0.3),因此若是foo执行超过0.3秒,则foo执行完后不会再等待0.3秒,而是直接继续下一个任务。若是foo执行不到0.3秒,则会等待到0.3秒再继续。这个方法的场景能够用在启动页动画,动画显示须要一个保证时间。
func attempt<T>(maximumRetryCount: Int = 3, delayBeforeRetry: DispatchTimeInterval = .seconds(2), _ body: @escaping () -> Promise<T>) -> Promise<T> {
var attempts = 0
func attempt() -> 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
}
复制代码
extension CLLocationManager {
static func promise() -> Promise<CLLocation> {
return PMKCLLocationManagerProxy().promise
}
}
class PMKCLLocationManagerProxy: NSObject, CLLocationManagerDelegate {
private let (promise, seal) = Promise<[CLLocation]>.pending()
private var retainCycle: PMKCLLocationManagerProxy?
private let manager = CLLocationManager()
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
//…
}
复制代码
retainCycle是其中一个循环引用,目的是为了避免让PMKCLLocationManagerProxy自身被释放,当Promise结束的时候,在ensure方法中执行self.retainCycle = nil
把引用解除,来达到释放自身的目的,很是巧妙。
有时候咱们须要传递任务中的一些中间结果,好比下面的例子,done中没法使用username变量:
login().then { username in
fetch(avatar: username)
}.done { image in
//…
}
复制代码
能够经过map巧妙的把结果变成元组形式返回:
login().then { username in
fetch(avatar: username).map { ($0, username) }
}.then { image, username in
//…
}
复制代码
尽管PromiseKit不少用法和原理都和Reactivecocoa类似,但它语法的简洁和直观是它最大的特色,光是这一点就足够吸引你们去喜欢它了~