PromiseKitとは
- http://promisekit.org/
- iOSプログラミングで頻繁に出てくる非同期処理を簡単かつエレガントにするライブラリ。
- JavaScriptとかでおなじみのPromiseパターンの実装と、各種CocoaフレームワークからPromiseを使うための拡張が含まれている。
- Objective-C版とSwift版がある。
使い方
NSURLConnection.GET("http://placekitten.com/250/250").then{ (img:UIImage) in
return CLGeocoder.geocode(addressString:"Mount Rushmore")
}.then { (placemark:CLPlacemark) in
return MKMapSnapshotter(options:opts).promise()
}.then { (snapshot:MKMapSnapshot) -> Promise<Int> in
let av = UIAlertView()
return av.promise()
}.then {
self.title = "You tapped button #\($0)"
}.then {
return CLLocationManager.promise()
}.catch { _ -> CLLocation in
return CLLocation(latitude: 41.89, longitude: -87.63)
}.then { (ll:CLLocation) -> Promise<NSDictionary> in
}.then
thenやcatchにクロージャを渡してメソッドチェーンしていく。これは普通のPromiseパターンと同じ。
- エラーが発生したら最も近い
catchで補足される。
tl;dr
NSURLConnection+PromiseKit.swiftのようなextensionが何種類か用意されている。
- 拡張されたメソッドは非同期処理を開始し、Promiseオブジェクトを初期化してすぐに返す。
- 非同期処理が成功すると、
fulfillerメソッドが実行される。
fulfillerメソッドは以下を実行する。
- Promiseオブジェクトの
statusを.Fulfilledに更新する。
handlersにあるクロージャをすべて実行する。
- Promiseオブジェクトの
thenメソッドを呼ぶと以下のようなクロージャがhandlersに追加され、新しいPromiseオブジェクトを返す。
thenメソッドの引数のクロージャを実行する。
- その返り値を
fulfillerに渡して実行する。
NSURLConnection+Promise.swift
public class func GET(url:String) -> Promise<NSData> {
// ...
}
- いくつかの拡張を見てみるとすべて
Promise<T>を返すようになってる。
- この返り値に対して
thenやcatchを呼んでいるので、これらのメソッドはPromiseクラスのメソッドだと考えられる。Promiseクラスについてはあとで見ていく。
public class func GET(url:String) -> Promise<UIImage> {
let rq = NSURLRequest(URL:NSURL(string:url))
return promise(rq)
}
- 冒頭の使い方のところで出てきた
UIImageを扱うメソッドはこれ。
NSURLRequestオブジェクトを作ってpromiseメソッドというのに渡して呼んでいる。
public class func promise(rq:NSURLRequest) -> Promise<UIImage> {
return fetch(rq) { (fulfiller, rejecter, data) in
}
}
- 引数に渡した
NSURLRequestオブジェクトをfetchメソッドに渡して呼び出している。
fetchメソッドはさらにクロージャを受け取っている。
func fetch<T>(var request: NSURLRequest, body: ((T) -> Void, (NSError) -> Void, NSData) -> Void) -> Promise<T> {
return Promise<T> { (fulfiller, rejunker) in
}
}
fetch内ではPromise<T>を初期化して返している。初期化時にまたもクロージャを渡している。
public init(_ body:(fulfiller:(T) -> Void, rejecter:(NSError) -> Void) -> Void) {
body(fulfiller, rejecter)
}
- 上のようなクロージャを受け取る初期化はこれのようだ。
- まず
bodyという引数を受け取る。bodyはfulfillerとrejecterの2つのクロージャを受け取ってVoidを返すクロージャ(ややこしい…)である。
- この
initでは引数として受け取ったbodyというクロージャを実行している。bodyに渡される2つの引数はinit内で定義される内部メソッドである。
public init(_ body:(fulfiller:(T) -> Void, rejecter:(NSError) -> Void) -> Void) {
func recurse() {
for handler in handlers { handler() }
handlers.removeAll(keepCapacity: false)
}
func rejecter(err: NSError) {
if self.pending {
self.state = .Rejected(err)
recurse()
}
}
func fulfiller(obj: T) {
if self.pending {
self.state = .Fulfilled(obj)
recurse()
}
}
body(fulfiller, rejecter)
}
fulfillerメソッドはstateを.Fulfilledに変更しrecurseを呼ぶ。
rejecterメソッドはstateを.Rejectedに変更しrecurseを呼ぶ。
recurseメソッドは、すべてのhandlerを実行したあと消去している。
func fetch<T>(var request: NSURLRequest, body: ((T) -> Void, (NSError) -> Void, NSData) -> Void) -> Promise<T> {
return Promise<T> { (fulfiller, rejunker) in
NSURLConnection.sendAsynchronousRequest(request, queue:PMKOperationQueue) { (rsp, data, err) in
if err {
rejecter(err)
} else {
body(fulfiller, rejecter, data!)
}
}
}
}
Promise<T>の初期化時に引数として渡されたクロージャが実行されるので、このときに非同期通信が実行されるようだ。
- 非同期通信が成功した場合、
body(fulfiller, rejecter, data!)が呼ばれる。このbodyというクロージャはfetchメソッドに渡されたもので、その中のfulfillerとrejecterの2つのクロージャはPromise<T>のinit内で定義されたメソッドである。
Promise.swift
public func then<U>(onQueue q:dispatch_queue_t = dispatch_get_main_queue(), body:(T) -> U) -> Promise<U> {
}
- シグネチャーがジェネリクスまみれで複雑。
dispatch_queue_t型と(T) -> U型を引数にとり、Promise<U>型を返すメソッドということになる。
TはPromiseクラスの型変数(←言い方合ってる?)であり、NSURLConnection+Promise.swiftの例で言うと、このTにはNSDataやNSStringが入ってくる。
- 例えば
TがNSDataの場合、第2引数のbodyは「NSDataを引数にとってUを返すクロージャ」となる。このUが例えばMKPlacemarkである場合、thenはPromise<MKPlacemark>を返すことになる。
- この返り値は
Promise<T>であるため再度thenを呼び出すことができメソッドチェーンが成立している。
public func then<U>(onQueue q:dispatch_queue_t = dispatch_get_main_queue(), body:(T) -> U) -> Promise<U> {
switch state {
case .Rejected(let error):
case .Fulfilled(let value):
case .Pending:
}
}
stateはPromise<T>クラスのプロパティでState<T>型として定義されている。
enum State<T> {
case Pending
case Fulfilled(@autoclosure () -> T)
case Rejected(NSError)
}
public func then<U>(onQueue q:dispatch_queue_t = dispatch_get_main_queue(), body:(T) -> U) -> Promise<U> {
switch state {
case .Rejected(let error):
case .Fulfilled(let value):
case .Pending:
}
}
stateはenum型であることが分かったので、thenに戻る。
- このswitch文ではvalue bindingsを行っている。マッチしたcase文で宣言された変数に値が割り当てられる。例えば、.Fulfilledにマッチした場合、stateを初期化する際に.Fulfilledに渡されたクロージャが
valueという変数に割り当てられる。
public func then<U>(onQueue q:dispatch_queue_t = dispatch_get_main_queue(), body:(T) -> U) -> Promise<U> {
switch state {
case .Pending:
return Promise<U>{ (fulfiller, rejecter) in
}
}
}
statusは宣言時に初期値として.Pendingを渡しているため、最初は.Pendingのcase文を通ることになりそう。
statusが.Pendingである場合、Promise<U>を初期化して返している。
- 初期化の際、引数にクロージャを渡している。上述の通り、渡されたクロージャは初期化処理の最後に実行される。
public func then<U>(onQueue q:dispatch_queue_t = dispatch_get_main_queue(), body:(T) -> U) -> Promise<U> {
switch state {
case .Pending:
return Promise<U>{ (fulfiller, rejecter) in
self.handlers.append{
switch self.state {
case .Fulfilled(let value):
fulfiller(value())
case .Rejected(let error):
dispatch_async(onQueue){ fulfiller(body(error)) }
case .Pending:
abort()
}
}
}
}
}
Promise<U>の初期化の最後でself.handlersにクロージャが追加されている。上述の通り、handlersはfulfillerとrejecter内で呼ばれるrecurseですべて実行される。
- つまり、
then()に渡されたクロージャはhandlersに追加され、そのPromiseオブジェクトの非同期処理が完了したときに呼ばれることになる。