iOS/UIViewControllerでTaskのキャンセル処理をやろう
環境
code:sh
$xcodebuild -version
Xcode 15.4
Build version 15F31d
モチベーション
Taskのキャンセルについての理解が甘かったのでメモ
/icons/hr.icon
UIViewControllerが破棄されてもTaskは処理され続ける
厳密にかくと、detached taskだとスレッドが別になるため処理され続ける
呼び出し元が消滅したからといってご丁寧にキャンセルしてくれる訳ではない
Task.init 経由で実行する処理はそれを呼ぶスレッドと同じスレッドで動作するため、キャンセルのことを考える必要はない
以下の場合は Task.init 内の処理が終わるまで viewDidLoad() が終わらない
code:swift
class SecondViewController: UIViewController {
var task: Task<Void, Error>?
override func viewDidLoad() {
super.viewDidLoad()
task = Task.init {
try? self.heavyCall() // viewDidLoad()と同じメインスレッドで実行される
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
task?.cancel()
}
nonisolated func heavyCall() throws {
var count = 0
while count < 1_000_000_000 {
count += 1
if Task.isCancelled {
print(count)
throw CancellationError()
}
}
}
}
こうする
detached taskとして呼び出し元とは別スレッドで実行するようにしても、無駄な処理は不要になった瞬間に停止されたいので、以下2つを実装して停止する
taskのキャンセル
task内で実行する処理の中でキャンセル確認してErrorを投げる
code:swift
class SecondViewController: UIViewController {
var task: Task<Void, Error>?
override func viewDidLoad() {
super.viewDidLoad()
task = Task.detached {
try? self.heavyCall()
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
task?.cancel()
}
nonisolated func heavyCall() throws {
var count = 0
while count < 1_000_000_000 {
count += 1
// 以下もしくは、try Task.checkCancellation()
if Task.isCancelled {
throw CancellationError()
}
}
}
}
Task.isCancelld + CancellationError() か Task.checkCancellation() の使い分けは前者はキャンセル時にエラーを投げる以外のことができるのでそこをカスタマイズしたいか否か