前回の
の続きです。今回は、拙作の「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
は、fulfill
orreject
を呼ぶまで、何回も実行できる(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
の処理の違いが、より見やすくなりました。
こちらも便利だと思うので、ぜひ使ってみてください。
(別の記法でもっと良いものがあったら、教えてください)