読者です 読者をやめる 読者になる 読者になる

PromiseKit/swiftを読んだ

Objective-C/Swift/iOS

PromiseKitとは

使い方

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
// ...
  • thencatchクロージャを渡してメソッドチェーンしていく。これは普通のPromiseパターンと同じ。
  • エラーが発生したら最も近いcatchで補足される。

tl;dr

  • NSURLConnection+PromiseKit.swiftのようなextensionが何種類か用意されている。
    • 拡張されたメソッドは非同期処理を開始し、Promiseオブジェクトを初期化してすぐに返す。
    • 非同期処理が成功すると、fulfillerメソッドが実行される。
  • fulfillerメソッドは以下を実行する。
    • Promiseオブジェクトのstatus.Fulfilledに更新する。
    • handlersにあるクロージャをすべて実行する。
  • Promiseオブジェクトのthenメソッドを呼ぶと以下のようなクロージャhandlersに追加され、新しいPromiseオブジェクトを返す。

NSURLConnection+Promise.swift

public class func GET(url:String) -> Promise<NSData> {
    // ...
}
  • いくつかの拡張を見てみるとすべてPromise<T>を返すようになってる。
  • この返り値に対してthencatchを呼んでいるので、これらのメソッド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
        // ...
    }
}
func fetch<T>(var request: NSURLRequest, body: ((T) -> Void, (NSError) -> Void, NSData) -> Void) -> Promise<T> {
    // ...

    return Promise<T> { (fulfiller, rejunker) in
        // ...
    }
}
  • fetch内ではPromise<T>を初期化して返している。初期化時にまたもクロージャを渡している。
// Promise.swift

public init(_ body:(fulfiller:(T) -> Void, rejecter:(NSError) -> Void) -> Void) {
    // ...
    body(fulfiller, rejecter)
}
  • 上のようなクロージャを受け取る初期化はこれのようだ。
  • まずbodyという引数を受け取る。bodyfulfillerrejecterの2つのクロージャを受け取ってVoidを返すクロージャ(ややこしい…)である。
  • このinitでは引数として受け取ったbodyというクロージャを実行している。bodyに渡される2つの引数はinit内で定義される内部メソッドである。
// Promise.swift

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メソッドに渡されたもので、その中のfulfillerrejecterの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にはNSDataNSStringが入ってくる。
  • 例えばTNSDataの場合、第2引数のbodyは「NSDataを引数にとってUを返すクロージャ」となる。このUが例えばMKPlacemarkである場合、thenPromise<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:
        // ...
    }
}
  • statePromise<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:
        // ...
    }
}
  • stateenum型であることが分かったので、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クロージャが追加されている。上述の通り、handlersfulfillerrejecter内で呼ばれるrecurseですべて実行される。
  • つまり、then()に渡されたクロージャhandlersに追加され、そのPromiseオブジェクトの非同期処理が完了したときに呼ばれることになる。