前回の
の続きです。今回は、拙作の「SwiftTask」というライブラリの紹介です。
SwiftTask
https://github.com/inamiy/SwiftTask
SwiftTaskについて
SwiftTaskは、PromiseKitやBolts-iOSと同じく、Promiseライクなフロー制御ライブラリです。
主に以下の特徴があります。
-
Progress、Pause/Resume、Cancelのインターフェースを新たに追加(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を初期化するにあたり、まずジェネリクスで表記されたProgress、Value (fulfilled)、Error (rejected)の型を決める必要があります(Progressは必ずしも、0.0〜1.0までのFloat型などである必要はありません)。
Task<Progress, Value, Error>
続いて、初期化クロージャを使って、引数で渡されるprogress、fulfill、reject、configureを内部で実行します。このとき、
-
fulfillとrejectの呼び出し方は、従来のPromiseの時と同じ -
progressは、fulfillorrejectを呼ぶまで、何回も実行できる(optional) -
configureで、pause、resume、cancelを実装する(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)を実行すると、taskにprogressClosureが登録されます。
そして、先ほどの初期化クロージャ内で引数のprogressハンドラが実行される度に、初期化クロージャの外側に位置するprogressClosureが実行される仕組みになっています。
ちなみに、task.progress()の返り値は、同じtaskなので、そのまま後続のthen/catchにつなぐことができます。
then
task.then(closure)は、 1つのclosure引数のみを受け取り、新しいtaskを返すメソッドです。
基本的に、taskが fulfilled になった場合は、closureが必ず呼ばれます。
ところで、JavaScript Promiseの場合、
-
promise.then(onFulfilled)(fulfilled only) -
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を返します。
taskが rejected になった場合のみ、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を使う
まだ試験的な実装ですが、progress、then、catchをカスタム演算子に置き換えることもできます。
-
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! ![]()
thenの処理の違いが、より見やすくなりました。
こちらも便利だと思うので、ぜひ使ってみてください。
(別の記法でもっと良いものがあったら、教えてください)