SwiftQueue を使ってみた

# はじめに
[SwiftQueue](https://github.com/lucas34/SwiftQueue) は [lucas34](https://github.com/lucas34) 氏が公開している iOS 向けの job scheduler ライブラリです。

job の実行条件を指定して、

 * 実行条件を満たしたときに自動で再実行
 * 成功するまで一定間隔でリトライ

などが可能です。
Android の [JobScheduler](https://developer.android.com/reference/android/app/job/JobScheduler) や [WorkManager の WorkRequest.Builder](https://developer.android.com/reference/androidx/work/WorkRequest.Builder) とよく似ていています。
（実は Android のこれらと同等の iOS ライブラリを探していて、 SwiftQueue を見つけました）

# 使いどころ
* リトライ制御
* モバイルデータ通信ではなく Wi-Fi 接続時にのみ行いたい処理の実行（写真等の同期）
* 定期実行処理（新着確認のポーリング）

**通信完了まで画面をブロックしない** アプリを提供するためには有用なライブラリだと見込んでいます。
詳しくは [インドのインターネット環境との戦い方](https://www.slideshare.net/kenichitatsuhama/ss-112660510) 参照

続編で、 [SwiftQueue の内部を追ってみた](https://qiita.com/tatsuhama/items/11b5c43c7a61310b5745) もあります。

# 詳細
※Github の [SwiftQueue](https://github.com/lucas34/SwiftQueue) より抜粋・補足
## 機能

* 逐次実行
* 並列実行
* キャンセル（すべて・ID 指定・タグ指定）
* 遅延実行
* 実行条件の指定（期日・インターネット接続・充電中）
* 先に同じ処理が実行されている場合の動作の選択
* リトライ（回数・一定時間後・exponential backoff：指数関数的後退）
* 定期実行

## 使い方

### Job の定義
```swift:SendTweetJob.swift
// Tweet を送信する Job
class SendTweetJob: Job {
    
    // この Job の識別子：JobCreator で job を生成する際に使用
    static let type = "SendTweetJob"
    // パラメータ
    private let tweet: [String: Any]

    required init(params: [String: Any]) {
        // JobBuilder.with() で指定されたパラメータを受け取る
        self.tweet = params
    }

    func onRun(callback: JobResult) {
        // 処理の実行
        let api = Api()
        api.sendTweet(data: tweet).execute(onSuccess: {
            // 成功したとき .success を callback に渡す
            callback.done(.success) 
        }, onError: { error in
            // 失敗したとき .fail を callback に渡す
            callback.done(.fail(error))
        })
    }

    func onRetry(error: Error) -> RetryConstraint {
        // エラー種別から、キャンセル or リトライを決める
        return error is ApiError ? RetryConstraint.cancel : RetryConstraint.retry(delay: 0)
    }

}
```

### JobCreator の定義
```swift:TweetJobCreator.kt
class TweetJobCreator: JobCreator {

    // type に応じて、実際の Job インスタンスを生成して返す
    func create(type: String, params: [String: Any]?) -> Job {
        if type == SendTweetJob.type  {
            return SendTweetJob(params: params)
        } else {
            // 合致しないときはエラーを返す
            fatalError("No Job !")
        }
    }
}
```

### 呼び出し
```swift
// job のキャンセルをしたい場合は、同じ manager インスタンスからキャンセルする必要がある
let manager = SwiftQueueManagerBuilder(creator: TweetJobCreator()).build()
JobBuilder(type: SendTweetJob.type)
        .internet(atLeast: .cellular) // インターネット接続を実行条件に指定
        .with(params: ["content": "Hello world"]) // パラメータ指定
        .schedule(manager: manager) // 実行予約（enqueue）
```
これらの実行を行うと、呼び出しは即時完了します。
（※ enqueue するだけで、投稿処理は完了していません！）

インターネット接続がある場合だと、job はすぐに実行されます。
インターネット接続がない場合は、job は保留され、インターネット接続が確保できれば自動で再実行されます。

# 試してみた機能
このコードをもとに試します。

```swift
let manager = SwiftQueueManagerBuilder(creator: MyJobCreator()).build()
for i in 1..<10 {
    JobBuilder(type:  MyJob.type)
        .with(params: ["key": "\(i)"])
        .schedule(manager: manager)
}
```

```swift:MyJob.swift
func onRun(callback: JobResult) {
    print("key = \(params?.first?.value as! String)") // パラメータの数字を print
    Thread.sleep(forTimeInterval: 1) // 1秒待つ
    callback.done(.success) // 成功
}

func onRemove(result: JobCompletion) {
    print("onRemove \(params?.first?.value as! String) is \(result)")
}
```

## 逐次実行
デフォルトでは1本の queue だけで実行される。

**実行結果**

```
key = 1
　(1秒後)
key = 2
　(1秒後)
key = 3
　(1秒後)
key = 4
　(1秒後)
```

## 並列実行
`.group(name:)` を指定するとその名前のグループの queue で実行されます。

```swift
JobBuilder(type:  MyJob.type)
    .group(name: "group\(i % 3)") // 3つのグループに振り分ける
    .with(params: ["key": "\(i)"])
    .schedule(manager: manager)
```

**実行結果**

```
key = 1
key = 2
key = 3
　(1秒後)
key = 5
key = 6
key = 4
　(1秒後)
key = 8
key = 9
key = 7
```
3並列で実行されているのがわかります。
（同時スタートなので、 5,6,4 などは順不同になる）

## 先に同じ処理が実行されている場合の動作の選択
job に ID を指定することで「同じ処理か」を識別する。
先の処理を上書きするか否かの選択ができる。

```swift
JobBuilder(type:  MyJob.type)
    .singleInstance(forId: "id\(i % 3)", override: false)
    .group(name: "group\(i % 3)")
    .with(params: ["key": "\(i)"])
    .schedule(manager: manager)
```

**実行結果（override:false）**
各 group 内で先勝ちになっています。

```
key = 1
key = 2
key = 3
onRemove 4 is fail(SwiftQueue.SwiftQueueError.duplicate)
onRemove 5 is fail(SwiftQueue.SwiftQueueError.duplicate)
onRemove 6 is fail(SwiftQueue.SwiftQueueError.duplicate)
onRemove 7 is fail(SwiftQueue.SwiftQueueError.duplicate)
onRemove 8 is fail(SwiftQueue.SwiftQueueError.duplicate)
onRemove 9 is fail(SwiftQueue.SwiftQueueError.duplicate)
```

**実行結果（override:true）**
各 group 内で後勝ちになっています。

```
key = 1
key = 2
key = 3
key = 6
key = 5
key = 4
key = 7
key = 8
key = 9
onRemove 5 is fail(SwiftQueue.SwiftQueueError.canceled)
onRemove 1 is fail(SwiftQueue.SwiftQueueError.canceled)
onRemove 6 is fail(SwiftQueue.SwiftQueueError.canceled)
onRemove 3 is fail(SwiftQueue.SwiftQueueError.canceled)
onRemove 2 is fail(SwiftQueue.SwiftQueueError.canceled)
onRemove 4 is fail(SwiftQueue.SwiftQueueError.canceled)
```

# 雑感など
* iOS アプリの job 実行管理がかなり便利になりそう
* group は Producer-Consumer のように、何本かの group を確保して、処理が終わった group に割り振るということはできなさそう…
* 画面をまたいだ場合や、アプリのライフサイクル（バックグラウンドの行き来や終了＆起動）時に自動復帰するのか要確認
* 条件が満たされたときの自動実行は、充電なら `NotificationCenter` に `UIDevice.batteryStateDidChangeNotification` を指定し、通信は `Reachability` ライブラリを使っていた
* 永続化されているみたいだけど、 [Android の WorkKamanger のように](https://qiita.com/tatsuhama/items/0b8d190665cd632f798f) クラス名をリファクタリングしたときの影響は…？

もうちょっと色々試してみます。