Help us understand the problem. What is going on with this article?

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の処理の違いが、より見やすくなりました。
こちらも便利だと思うので、ぜひ使ってみてください。
(別の記法でもっと良いものがあったら、教えてください)

inamiy
iOS Developer
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away