はじめに
SwiftQueue は lucas34 氏が公開している iOS 向けの job scheduler ライブラリです。
job の実行条件を指定して、
- 実行条件を満たしたときに自動で再実行
- 成功するまで一定間隔でリトライ
などが可能です。
Android の JobScheduler や WorkManager の WorkRequest.Builder とよく似ていています。
(実は Android のこれらと同等の iOS ライブラリを探していて、 SwiftQueue を見つけました)
使いどころ
- リトライ制御
- モバイルデータ通信ではなく Wi-Fi 接続時にのみ行いたい処理の実行(写真等の同期)
- 定期実行処理(新着確認のポーリング)
通信完了まで画面をブロックしない アプリを提供するためには有用なライブラリだと見込んでいます。
詳しくは インドのインターネット環境との戦い方 参照
続編で、 SwiftQueue の内部を追ってみた もあります。
詳細
※Github の SwiftQueue より抜粋・補足
機能
- 逐次実行
- 並列実行
- キャンセル(すべて・ID 指定・タグ指定)
- 遅延実行
- 実行条件の指定(期日・インターネット接続・充電中)
- 先に同じ処理が実行されている場合の動作の選択
- リトライ(回数・一定時間後・exponential backoff:指数関数的後退)
- 定期実行
使い方
Job の定義
// 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 の定義
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 !")
}
}
}
呼び出し
// 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 は保留され、インターネット接続が確保できれば自動で再実行されます。
試してみた機能
このコードをもとに試します。
let manager = SwiftQueueManagerBuilder(creator: MyJobCreator()).build()
for i in 1..<10 {
JobBuilder(type: MyJob.type)
.with(params: ["key": "\(i)"])
.schedule(manager: manager)
}
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 で実行されます。
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 を指定することで「同じ処理か」を識別する。
先の処理を上書きするか否かの選択ができる。
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 のように クラス名をリファクタリングしたときの影響は…?
もうちょっと色々試してみます。