Edited at

SwiftTask(Promise拡張)を使う

More than 3 years have passed since last update.

前回の


  1. Swiftで有限オートマトン(ステートマシン)を作る - Qiita

  2. Swift+有限オートマトンでPromiseを拡張する - Qiita

の続きです。今回は、拙作の「SwiftTask」というライブラリの紹介です。

SwiftTask

https://github.com/inamiy/SwiftTask


SwiftTaskについて

SwiftTaskは、PromiseKitBolts-iOSと同じく、Promiseライクなフロー制御ライブラリです。

主に以下の特徴があります。



  • ProgressPause/ResumeCancelのインターフェースを新たに追加(Promiseを高機能化)

  • ジェネリクスを活用し、ほぼ Pure Swift (Cocoa Frameworkに依存しない)

  • SwiftTask単体は 非同期(スレッド、イベントループ)処理を持たない、薄いラッパークラス


    • 基本的に、非同期処理はタスク内部(初期化クロージャ)で行う(後述のAlamofireの例を参考)

    • 複数タスクにまたがる協調的な処理(Task.all()など)では、最低限の排他制御のみを行う



  • 引数1つのtrailing closureを使った、簡易的な method chaining

他ライブラリとの比較は、SwiftTask自身に非同期処理がないため簡単ではありませんが、参考までに表にします(2014/08/30現在。個人的な感想です)

ライブラリ
progress
pause
cancel
直列処理
協調的処理

NSOperation/Queue
×

実行中は止まらない

×
addDependencyつらい
×
addDependencyつらい

PromiseKit
×
×
×

thenの実装が不十分?

whenのみ

Bolts-iOS
×
×

外部token方式


allのみ

SwiftTask

インターフェースのみ

インターフェースのみ

インターフェースのみ


all, any, someなど


使用例

SwiftTaskでタスクを定義する際(初期化クロージャ内)、直接Grand Central Dispatch等を使って非同期処理を行うことも可能です。

が、Alamofire(ネットワークライブラリ)に代表されるような、



  • obj.setProgressHandler() (callback)


  • obj.setCompletionHandler() (callback)

  • obj.pause()

  • obj.resume()

  • obj.cancel()

のAPIの形に「別クラスで一旦ラップする」方が、より簡単に実装することができます。


Alamofireを使った例

typealias Progress = (bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64)

// define task
let task = Task<Progress, NSData?, NSError> { (progress, fulfill, reject, configure) in

let request = Alamofire.download(.GET, "http://httpbin.org/stream/100", destination: somewhere)

request.progress { (bytesWritten, totalBytesWritten, totalBytesExpectedToWrite) in

progress((bytesWritten, totalBytesWritten, totalBytesExpectedToWrite) as Progress)

}.response { (request, response, data, error) in

if let error = error {
reject(error)
return
}

fulfill(data as NSData?)

}

configure.pause = { request.suspend() }
configure.resume = { request.resume() }
configure.cancel = { request.cancel() }

return
}

上のコードは、NSURLSessionDownloadTaskを内部で使った、簡単なダウンロード処理のサンプルです。

taskを初期化するにあたり、まずジェネリクスで表記されたProgressValue (fulfilled)Error (rejected)の型を決める必要があります(Progressは必ずしも、0.0〜1.0までのFloat型などである必要はありません)。

Task<Progress, Value, Error>

続いて、初期化クロージャを使って、引数で渡されるprogressfulfillrejectconfigureを内部で実行します。このとき、



  • fulfillrejectの呼び出し方は、従来のPromiseの時と同じ


  • progressは、fulfill or rejectを呼ぶまで、何回も実行できる(optional)


  • configureで、pauseresumecancelを実装する(optional)

なお、Alamofireは、元々Cocoaがデリゲートメソッドで用意しているprogress部分を コールバック形式に変換している ので、Alamofire → SwiftTaskへのprogressの伝搬が上述のように非常に簡単に書けます。


Method Chaining

Promiseのようにmethod chainingをする方法は、次の通りです。

task.progress { progress in

println("bytesWritten = \(progress.bytesWritten)")
println("totalBytesWritten = \(progress.totalBytesWritten)")
println("totalBytesExpectedToWrite = \(progress.totalBytesExpectedToWrite)")

}.then { (value: NSData?) -> Void in

println("fulfilled: \(value)")

}.catch { (errorInfo: ErrorInfo) -> Void in

println("rejected: \(errorInfo.error) \(errorInfo.isCancelled)")

}


progress

まず、task.progress(progressClosure)を実行すると、taskprogressClosureが登録されます。

そして、先ほどの初期化クロージャ内で引数のprogressハンドラが実行される度に、初期化クロージャの外側に位置するprogressClosureが実行される仕組みになっています。

ちなみに、task.progress()の返り値は、同じtaskなので、そのまま後続のthen/catchにつなぐことができます。


then

task.then(closure)は、 1つのclosure引数のみを受け取り、新しいtaskを返すメソッドです。

基本的に、taskfulfilled になった場合は、closureが必ず呼ばれます。

ところで、JavaScript Promiseの場合、



  1. promise.then(onFulfilled) (fulfilled only)


  2. promise.then(onFulfilled, onRejected) (fulfilled & rejected)

のように、最大2つの引数を取りながら両者を区別していますが、SwiftTaskでは 引数を1つのclosureに制限+オーバーロード+型推論 して区別しています。このメリットとして、


  • いつでもtask.then {...}.then {...}の形で書ける(()がいらない)


  • onFulfilled/onRejectedに分けるより、Apple流のcompletionHandlerに統一した方が、APIデザイン的に良い



があります。

thenが受け取るclosureの型の詳細については、README.mdをご覧ください。


catch

thenと同様、task.catch(closure)は、 1つのclosure引数を受け取り、新しいtaskを返します。

taskrejected になった場合のみ、closureが呼ばれます。

ここで、closureの引数はErrorオブジェクトではなく、Task.ErrorInfoという(error: Error?, isCancelled: Bool)のTuple型になっています。

errorInfo.isCancelledを見ることによって、キャンセルによるrejectedかどうかの判断ができます。


Pause/Resume/Cancel

初期化クロージャのconfigureで設定したpause/resume/cancelは、外側からtaskオブジェクトの各メソッドを直接呼ぶことで実行できます。

task.pause()

task.resume()
task.cancel() // task.cancel(error)もできる

キャンセルされたtaskは、内部でrejectされたものとほぼ同じ形で、catchなどで捕捉することができます。


番外:Custom Operatorを使う

まだ試験的な実装ですが、progressthencatchをカスタム演算子に置き換えることもできます。



  • task ~ {...} = task.progress { progress in ...}


  • task >>> {...} = task.then { value, errorInfo in ...} (fulfilled & rejected)


  • task *** {...} = task.then { value in ...} (fulfilled only)


  • task !!! {...} = task.catch { errorInfo in ...} (rejected only)

さっそく試してみましょう。

task ~ { progress in

...
} *** { (value: NSData?) -> String in
...
} !!! { (errorInfo: ErrorInfo) -> String in
...
} >>> { (value, errorInfo) -> Void in
...
}

Ship it!:shipit: thenの処理の違いが、より見やすくなりました。

こちらも便利だと思うので、ぜひ使ってみてください。

(別の記法でもっと良いものがあったら、教えてください)