Swiftの取消(Cancellation)とエラーハンドリングを徹底解説!構造化並行性で理解する非同期処理の基本
生徒
「Swiftで非同期処理を学んでいるんですが、途中で処理をやめたいときはどうすればいいんですか?」
先生
「それは『取消(キャンセル)』という仕組みを使います。例えば、動画をダウンロードしている途中でユーザーがアプリを閉じた場合、処理を続けるのは無駄ですよね。そのときにキャンセルを行います。」
生徒
「なるほど!じゃあキャンセルをするときにエラーになることもあるんですか?」
先生
「そうです。Swiftではキャンセルも一種のエラーとして扱います。そして構造化並行性(Structured Concurrency)を使うと、タスク全体を安全に管理できるんですよ。」
1. Swiftの取消(Cancellation)とは?
取消(キャンセル)は、「もうこの処理は必要ない」と判断したときに途中でやめる仕組みです。例えば、スマホで音楽をストリーミング再生していて、曲をスキップするときに前の曲のデータ取得を止める、といった場面を想像するとわかりやすいでしょう。
キャンセルを行わないと、裏で無駄な処理が続いてしまい、アプリが遅くなったり、バッテリーを消費してしまいます。そのためSwiftではTaskにキャンセルの仕組みが備わっており、効率的なプログラムを書くことができます。
2. Taskのキャンセルの基本
Swiftの非同期処理はTaskで動きます。キャンセルをする場合はtask.cancel()を呼び出し、タスクの中でキャンセルがリクエストされているかを確認します。
let task = Task {
for i in 1...5 {
try Task.checkCancellation()
print("処理中: \(i)")
try await Task.sleep(nanoseconds: 1_000_000_000)
}
return "完了"
}
Task {
try await Task.sleep(nanoseconds: 2_000_000_000)
task.cancel()
}
処理中: 1
処理中: 2
この例では、2秒後にタスクをキャンセルしています。Task.checkCancellation()でキャンセルを確認し、リクエストがあればエラーを投げて処理を終了します。
3. キャンセルはエラーの一種
Swiftではキャンセルは特別なエラーとして扱われます。キャンセルが発生するとCancellationErrorが投げられます。これは「問題が起きた」というより「処理をやめた」という意味合いのエラーです。
Task {
do {
try Task.checkCancellation()
print("処理を実行中")
} catch is CancellationError {
print("キャンセルされました")
} catch {
print("その他のエラー: \(error)")
}
}
キャンセルされました
このように、キャンセルは明確に区別して扱うことで、「処理が失敗した」のか「ユーザーがやめた」のかを判断できます。
4. 構造化並行性とキャンセル
Swiftの構造化並行性(Structured Concurrency)は、タスクを親子関係で管理する仕組みです。親タスクがキャンセルされると、その子タスクもまとめてキャンセルされます。これは現実の「チーム作業」に似ています。リーダーが「今日は作業中止!」といえば、全員が作業をやめるイメージです。
await withTaskGroup(of: String.self) { group in
group.addTask {
try Task.checkCancellation()
return "タスク1完了"
}
group.addTask {
try Task.checkCancellation()
return "タスク2完了"
}
group.cancelAll() // すべての子タスクをキャンセル
for await result in group {
print(result)
}
}
(出力なし)
group.cancelAll()を呼び出すと、追加されたタスクがすべてキャンセルされます。これにより、安全に処理を止められます。
5. キャンセルとエラー設計のポイント
キャンセルとエラーは似ていますが、意味が異なります。エラーは「問題が起きたために失敗した」、キャンセルは「意図的にやめた」です。そのため、エラー処理の設計ではキャンセルを特別に扱うことが推奨されます。
- ユーザー操作による中断はキャンセル扱いにする
- ネットワーク切断やファイル不足はエラー扱いにする
- キャンセルを検知したらすぐに処理を終了する
こうすることで、ユーザーにとって「予期せぬ失敗」と「自分でやめた」を区別でき、アプリの使いやすさが向上します。
6. 実際のアプリでの利用例
例えば動画アプリを考えてみましょう。ユーザーが再生中に別の動画を選択した場合、前の動画の読み込みはキャンセルされます。一方で、通信環境が悪くて読み込めなかった場合はエラーです。この違いを正しく設計することで、アプリが直感的で信頼できる動きをするようになります。
Swiftの構造化並行性とキャンセル処理を組み合わせることで、大規模なアプリでも効率よく安全に非同期処理を管理できます。
まとめ
Swiftの非同期処理における取消(キャンセル)とエラーハンドリングについて、全体を振り返ると、構造化並行性がもたらす「安全に終わらせる仕組み」の重要性がよく見えてきます。タスクの途中で処理が不要になったとき、SwiftではTaskを通じて自然にキャンセルが伝播し、CancellationErrorとして扱うことで「意図的な中断」と「予期せぬ失敗」の違いを丁寧に整理できます。アプリ開発の場面でも、動画の読み込みや音楽ストリーミングなど、ユーザー操作によって不要になる処理は珍しくありません。そうした状況に対応するためのキャンセル機能は、今やSwiftで快適なアプリ体験を作る上で不可欠な要素です。
また、キャンセルは他のエラーと区別するべきであり、それを特別に扱うことで、ユーザーに「やめた」理由を明確に伝えられます。構造化並行性を使うと、親から子へキャンセルが連鎖し、複数のタスクが動く場面でも整合性と安全性を保ちながら処理を制御できます。こうした仕組みは、開発者にとってもユーザーにとっても嬉しいメリットをもたらします。
ここでは、実際のSwiftアプリ開発で役立つようなサンプルコードを交えながら、取消とエラー処理の基礎をまとめました。構造化並行性の動き方や、タスクキャンセルの実装方法を身につけることで、より強固で使いやすいアプリを開発できるようになるはずです。非同期処理の根本を理解しておくことで、タスクの競合やリソースの無駄を防ぎ、アプリ全体のパフォーマンスと品質を高めることができます。
キャンセル処理のサンプルコード再整理
class DataLoader {
func fetch() async throws -> String {
for i in 1...5 {
try Task.checkCancellation()
print("読み込み中: \(i)")
try await Task.sleep(nanoseconds: 800_000_000)
}
return "読み込み完了"
}
}
Task {
let loader = DataLoader()
let task = Task {
try await loader.fetch()
}
try await Task.sleep(nanoseconds: 1_500_000_000)
task.cancel()
do {
let result = try await task.value
print(result)
} catch is CancellationError {
print("読み込みはキャンセルされました")
}
}
こうした具体例は、実際のアプリで複数の非同期タスクを処理する際にも応用できます。キャンセルを適切に扱うことで、ユーザーが操作した通りの反応を返せる自然な挙動につながり、アプリ全体が軽快になります。
生徒
「今日学んだキャンセル処理って、アプリの動きを自然にするためにとても大事なんですね。ユーザーが操作したときに無駄な処理が続かないのは良い設計だと感じました。」
先生
「その通りです。キャンセルは単なる停止ではなく、アプリを効率よく動かすための仕組みなんです。構造化並行性と合わせて使うことで、タスク全体の整合性を保ちながら安全に処理を終わらせられます。」
生徒
「エラーとキャンセルを分けて扱う理由もよく分かりました。たしかに“やめた”のと“失敗した”のでは意味が違いますもんね。」
先生
「その理解はとても良いですね。開発の現場では、その違いを丁寧に扱うことでユーザー体験を大きく向上させられます。Swiftのキャンセル処理は強力なので、これからも活用していきましょう。」