axiom-swift-concurrency について
Swift 開発者向けに、非同期処理から並行処理、Actor へ進む方法と Swift 6 の安全性を解説するガイドです。スレッドではなく隔離ドメインを基準に設計します。
主な機能
- 01段階的な並行処理の解説
- 02async/await とタスクの説明
- 03Actor と隔離の解説
- 04Sendable とデータ競合の対策
- 05CPU 処理をバックグラウンドで行う方法
活用シーン
- 01Swift プロジェクトの並行処理方針の策定
- 02Actor 隔離や Sendable エラーの調査
- 03デリゲートコールバックの非同期化
- 04CPU 負荷の高い処理をメインから移動
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-swift-concurrencyインストール後、Claude Code で/axiom-swift-concurrencyを入力して読み込まれたか確認します。
詳細
Swift 6 並行処理ガイド
目的:シングルスレッドから並行処理を使った Swift コードへ段階的に進みます
Swift バージョン:Swift 6.3(厳密な並行処理チェックがデフォルトで有効)。@concurrent には Swift 6.2 以降が必要です。
iOS バージョン:iOS 17 以降(@concurrent には iOS 26 以降)
Xcode:Xcode 16 以降(@concurrent には Xcode 26 以降)
このスキルを使用する場面
✅ このスキルを使用する場面:
- 新しいプロジェクトを開始し、並行処理の方針を決めるとき
- Swift 6 の並行処理エラー(アクターの隔離、データ競合、Sendable の警告)をデバッグするとき
- async/await を導入するタイミングや、並行処理を導入するタイミングを決めるとき
@MainActorクラスや非同期関数を実装するとき- デリゲートコールバックを非同期処理に安全なパターンに変換するとき
@MainActor、nonisolated、@concurrent、アクター隔離のいずれを使うか決めるとき- 「Sending 'self' risks causing data races」エラーを解決するとき
- 型を
Sendableに準拠させるとき - CPU 負荷の高い処理をバックグラウンドスレッドに移すとき
- UI の応答性が悪く、プロファイリングでメインスレッドがボトルネックと判明したとき
❌ このスキルを使用しない場面:
- 一般的な Swift の構文(Swift のドキュメントを使用してください)
- SwiftUI 固有のパターン(
axiom-swiftui-debuggingまたはaxiom-swiftui-performanceを使用してください) - API 固有のパターン(API のドキュメントを使用してください)
基本理念:スレッドではなく隔離ドメインで考える
「アプリはまず、すべてのコードをメインスレッドで実行するところから始めるべきです。シングルスレッドのコードでも、かなりのことができます。」 — Apple
考えるのをやめること:「これはどのスレッドで実行すべきか?」 考え始めること:「この処理を所有すべき隔離ドメインはどれか?」
@MainActor→ UI 状態の所有- カスタム
actor→ 共有可変状態の所有 nonisolated→ 所有者なし。呼び出し元が決定@concurrent→ バックグラウンドでの実行を強制
非同期はバックグラウンドを意味しません。 async 関数はブロックせずに中断しますが、呼び出されたときと同じアクター上で再開します。@MainActor の非同期関数はメインアクター上で最後まで実行されます。await は制御を譲るだけで、スレッドを切り替えるわけではありません。呼び出し元のアクター外で処理を実行する必要がある場合は、@concurrent(Swift 6.2 以降)を使用してください。
構造化並行処理を優先してください。 並列処理には async let と TaskGroup を使用します。タスクツリーを通じてキャンセルとエラーが自動的に伝播します。非構造化の Task {} は、同期から非同期への境界(イベントハンドラー、SwiftUI の .task)をつなぐために使用します。Task.detached は最終手段です。
GCD はブリッジパターンであり、デフォルトではありません。 新しいコードでは、主要なアーキテクチャとして DispatchQueue、DispatchGroup、DispatchSemaphore、完了ハンドラーを使用しないでください。非同期の代替手段がまだないレガシー API をつなぐ場合にのみ使用してください。ブリッジコードを隔離し、コードベースの残りの部分では Swift 6 の慣用的な書き方を維持してください。
段階的な進め方
Single-Threaded → Asynchronous → Concurrent → Actors
↓ ↓ ↓ ↓
Start here Hide latency Background Move data
(network) CPU work off main
次の段階に進むタイミング:
- UI の応答性が保たれ、処理が速い場合は、シングルスレッドのままにします
- 高レイテンシの処理(ネットワーク、ファイル I/O)が UI をブロックする場合は、async/await を追加します
- CPU 負荷の高い処理(画像処理、解析)が UI をフリーズさせる場合は、並行処理を追加します
- メインアクター上のコードが多すぎて競合が起きる場合は、アクターを追加します
重要なポイント:並行処理を使うコードは複雑になります。プロファイリングで必要性が確認された場合にのみ、並行処理を導入してください。
ステップ 1:シングルスレッドのコード(ここから始める)
Main Actor Mode が有効な場合(Xcode 26 以降の新規プロジェクトではデフォルト)、明示的に別の指定がない限り、すべてのコードはメインスレッドで実行されます。
// ✅ Simple, single-threaded
class ImageModel {
var imageCache: [URL: Image] = [:]
func fetchAndDisplayImage(url: URL) throws {
let data = try Data(contentsOf: url) // Reads local file
let image = decodeImage(data)
view.displayImage(image)
}
func decodeImage(_ data: Data) -> Image {
// Decode image data
return Image()
}
}
Main Actor Mode(Xcode 26 以降)
- 新規プロジェクトではデフォルトで有効です
- 明示的に別の指定がない限り、すべてのコードは
@MainActorによって保護されます - 並行アクセスを気にせず、共有状態に安全にアクセスできます
ビルド設定(Xcode 26 以降)
Build Settings → Swift Compiler — Language
→ "Default Actor Isolation" = Main Actor
Build Settings → Swift Compiler — Upcoming Features
→ "Approachable Concurrency" = Yes
これで十分な場合:すべての処理が高速(60 fps で 16 ms 未満)なら、シングルスレッドのままにします!
ステップ 2:非同期タスク(レイテンシを隠す)
データ(ネットワーク、ファイル I/O)の待機によって UI がフリーズする場合は、async/await を追加します。
問題:ネットワークアクセスが UI をブロックする
// ❌ Blocks main thread until network completes
func fetchAndDisplayImage(url: URL) throws {
let data = try Data(contentsOf: url) // ❌ Synchronous network fetch, freezes UI!
let image = decodeImage(data)
view.displayImage(image)
}
解決策:Async/Await
// ✅ Suspends without blocking main thread
func fetchAndDisplayImage(url: URL) async throws {
let (data, _) = try await URLSession.shared.data(from: url) // ✅ Suspends here
let image = decodeImage(data) // ✅ Resumes here when data arrives
view.displayImage(image)
}
何が起きるか:
- 関数がメインスレッドで開始します
awaitによって関数が一時停止し、メインスレッドはブロックされません- URLSession がバックグラウンドスレッドでデータを取得します(ライブラリが処理します)
- データが届くと、関数がメインスレッドで再開します
- その間、UI は応答性を保ちます
タスクの作成
ユーザーイベントに応じてタスクを作成します:
class ImageModel {
var url: URL = URL(string: "https://swift.org")!
func onTapEvent() {
Task { // ✅ Create task for user action
do {
try await fetchAndDisplayImage(url: url)
} catch {
displayError(error)
}
}
}
}
タスクのインターリーブ(重要な概念)
複数の非同期タスクは、順番に実行することで同じスレッド上で動作できます:
Task 1: [Fetch Image] → (suspend) → [Decode] → [Display]
Task 2: [Fetch News] → (suspend) → [Display News]
Main Thread Timeline:
[Fetch Image] → [Fetch News] → [Decode Image] → [Display Image] → [Display News]
メリット:
- メインスレッドがアイドル状態のままになりません
- タスクは可能な限り早く進行します
- まだ並行処理ではなく、シングルスレッドのままです!
重要:上記の両方のタスクはメインアクター上で実行されます。await キーワードはタスクを一時停止してスレッドを解放しますが、タスクが再開すると、同じ隔離ドメイン(メインアクター)に戻ります。await する API(URLSession など)が内部で処理しない限り、バックグラウンドスレッドは使用されません。
タスクを使用する場面:
- レイテンシの高い処理(ネットワーク、ファイル I/O)
- ライブラリ API がバックグラウンド処理を担う場合(URLSession、FileManager)
- 独自のコードをメインスレッドで実行し続ける場合
ステップ 3:並行コード(バックグラウンドスレッド)
CPU 負荷の高い処理が UI をブロックする場合に、並行処理を追加します。
問題:デコードが UI をブロックする
プロファイリングの結果、decodeImage() に 200ms かかり、UI の表示が乱れることが分かりました:
func fetchAndDisplayImage(url: URL) async throws {
let (data, _) = try await URLSession.shared.data(from: url)
let image = decodeImage(data) // ❌ 200ms on main thread!
view.displayImage(image)
}
解決策 1:@concurrent 属性(Swift 6.2 以降)
関数を常にバックグラウンドスレッドで実行するよう強制します:
func fetchAndDisplayImage(url: URL) async throws {
let (data, _) = try await URLSession.shared.data(from: url)
let image = await decodeImage(data) // ✅ Runs on background thread
view.displayImage(image)
}
@concurrent
func decodeImage(_ data: Data) async -> Image {
// ✅ Always runs on background thread pool
// Good for: image processing, file I/O, parsing
return Image()
}
@concurrent の動作:
- 関数は常にバックグラウンドスレッドプールに切り替わります
- コンパイラがメインアクターのデータアクセスを強調表示します(修正が必要な箇所を示します)
awaitなしでは@MainActorプロパティにアクセスできません
要件:Swift 6.2 以降、Xcode 26 以降、iOS 26 以降
解決策 2:nonisolated(ライブラリ API)
汎用 API を提供する場合は、代わりに nonisolated を使用します:
// ✅ Stays on caller's actor
nonisolated
func decodeImage(_ data: Data) -> Image {
// Runs on whatever actor called it
// Main actor → stays on main actor
// Background → stays on background
return Image()
}
nonisolated を使用する場面:
- 作業をどこで実行するかを呼び出し元が決めるライブラリ API
- メインスレッドで実行しても問題ない小規模な処理
- さまざまなコンテキストで使われる汎用コード
@concurrent を使用する場面:
- 常にバックグラウンドで実行すべき処理(画像処理、解析)
- UI をブロックしてはならない、パフォーマンスが重要な処理
メインアクターとの結び付きを解消する
関数に @concurrent を付けると、コンパイラがメインアクターへのアクセスを表示します:
@MainActor
class ImageModel {
var cachedImage: [URL: Image] = [:] // Main actor data
@concurrent
func decodeImage(_ data: Data, at url: URL) async -> Image {
if let image = cachedImage[url] { // ❌ Error: main actor access!
return image
}
// decode...
}
}
方法 1:呼び出し元に移す
func fetchAndDisplayImage(url: URL) async throws {
// ✅ Check cache on main actor BEFORE async work
if let image = cachedImage[url] {
view.displayImage(image)
return
}
let (data, _) = try await URLSession.shared.data(from: url)
let image = await decodeImage(data) // No URL needed now
view.displayImage(image)
}
@concurrent
func decodeImage(_ data: Data) async -> Image {
// ✅ No main actor access needed
return Image()
}
方法 2:@MainActor ヘルパー経由でアクセスする
@MainActor
func getCachedImage(for url: URL) -> Image? {
cachedImage[url]
}
@concurrent
func decodeImage(_ data: Data, at url: URL) async -> Image {
// ✅ Call @MainActor function to access isolated state
if let image = await getCachedImage(for: url) {
return image
}
// decode...
}
方法 3:nonisolated にする
nonisolated
func decodeImage(_ data: Data) -> Image {
// ✅ No actor isolation, can call from anywhere
return Image()
}
並行スレッドプール
バックグラウンドで処理を実行する場合:
Main Thread: [UI] → (suspend) → [UI Update]
↓
Background Pool: [Task A] → [Task B] → [Task A resumes]
Thread 1 Thread 2 Thread 3
重要なポイント:
- スレッドプールのサイズはシステムが管理します(Watch では 1~2 スレッド、Mac では多数)
- タスクは開始時とは異なるスレッドで再開されることがあります
- どのスレッドを使用するかを指定する必要はありません。システムが自動的に最適化します
ステップ 4:アクター(メインスレッドからデータを移す)
メインアクター上で実行されるコードが多すぎて競合が発生している場合は、アクターを追加します。
問題:メインアクターの競合
@MainActor
class ImageModel {
var cachedImage: [URL: Image] = [:]
let networkManager: NetworkManager = NetworkManager() // ❌ Also @MainActor
func fetchAndDisplayImage(url: URL) async throws {
// ✅ Background work...
let connection = await networkManager.openConnection(for: url) // ❌ Hops to main!
let data = try await connection.data(from: url)
await networkManager.closeConnection(connection, for: url) // ❌ Hops to main!
let image = await decodeImage(data)
view.displayImage(image)
}
}
問題点:バックグラウンドタスクがネットワークマネージャーにアクセスするため、何度もメインアクターに切り替わります。
解決策:ネットワークマネージャーアクター
// ✅ Move network state off main actor
actor NetworkManager {
var openConnections: [URL: Connection] = [:]
func openConnection(for url: URL) -> Connection {
if let connection = openConnections[url] {
return connection
}
let connection = Connection()
openConnections[url] = connection
return connection
}
func closeConnection(_ connection: Connection, for url: URL) {
openConnections.removeValue(forKey: url)
}
}
@MainActor
class ImageModel {
let networkManager: NetworkManager = NetworkManager()
func fetchAndDisplayImage(url: URL) async throws {
// ✅ Now runs mostly on background
let connection = await networkManager.openConnection(for: url)
let data = try await connection.data(from: url)
await networkManager.closeConnection(connection, for: url)
let image = await decodeImage(data)
view.displayImage(image)
}
}
変更点:
NetworkManagerは@MainActor classではなくactorになりました- ネットワークの状態が独自のアクター内で隔離されます
- バックグラウンドコードはメインアクターに切り替わらずにネットワークマネージャーへアクセスできます
- UI の処理に使えるよう、メインスレッドが解放されます
アクターを使用する場面
✅ アクターを使用する場合:
- 独立した状態を持つ UI 以外のサブシステム(ネットワークマネージャー、キャッシュ、データベース)
- メインアクターの競合を引き起こしているデータ
- UI コードから関心事を分離する場合
❌ アクターを使用しない場合:
- UI に関わるクラス(ViewModels、View Controllers)→
@MainActorを使用します - UI で使用されるモデルクラス →
@MainActorまたは非 Sendable のままにします - アプリ内のすべてのクラス(アクターは複雑さを増します)
ガイドライン:まずプロファイリングを行います。ボトルネックの原因となる状態がメインアクターに多すぎる場合は、サブシステムを一度に 1 つずつアクターに切り出します。
Sendable 型(アクター境界を越えるデータ)
アクター間またはタスク間でデータを渡すとき、Swift はそのデータがSendable(安全に共有できる)かどうかを確認します。
値型は Sendable です
// ✅ Value types copy when passed
let url = URL(string: "https://swift.org")!
Task {
// ✅ This is a COPY of url, not the original
// URLSession.shared.data runs on background automatically
let data = try await URLSession.shared.data(from: url)
}
// ✅ Original url unchanged by background task
安全な理由:各アクターはそれぞれ独立したコピーを持ちます。変更はほかのコピーに影響しません。
Sendable とは?
// ✅ Basic types
extension URL: Sendable {}
extension String: Sendable {}
extension Int: Sendable {}
extension Date: Sendable {}
// ✅ Collections of Sendable elements
extension Array: Sendable where Element: Sendable {}
extension Dictionary: Sendable where Key: Sendable, Value: Sendable {}
// ✅ Structs/enums with Sendable storage
struct Track: Sendable {
let id: String
let title: String
let duration: TimeInterval
}
enum PlaybackState: Sendable {
case stopped
case playing
case paused
}
// ✅ Main actor types
@MainActor class ImageModel {} // Implicitly Sendable (actor protects state)
// ✅ Actor types
actor NetworkManager {} // Implicitly Sendable (actor protects state)
参照型(クラス)と Sendable
// ❌ Classes are NOT Sendable by default
class MyImage {
var width: Int
var height: Int
var pixels: [Color]
func scale(by factor: Double) {
// Mutates shared state
}
}
let image = MyImage()
let otherImage = image // ✅ Both reference SAME object
image.scale(by: 0.5) // ✅ Changes visible through otherImage!
並行処理での問題:
func scaleAndDisplay(imageName: String) {
let image = loadImage(imageName)
Task {
image.scale(by: 0.5) // Background task modifying
}
view.displayImage(image) // Main thread reading
// ❌ DATA RACE! Both threads could touch same object!
}
解決策 1:送信前に変更を完了する
@concurrent
func scaleAndDisplay(imageName: String) async {
let image = loadImage(imageName)
image.scale(by: 0.5) // ✅ All modifications on background
image.applyAnotherEffect() // ✅ Still on background
await view.displayImage(image) // ✅ Send to main actor AFTER modifications done
// ✅ Main actor now owns image exclusively
}
解決策 2:クラスを同時に共有しない
同時アクセスを防ぐため、モデルクラスを @MainActor にするか、Sendable に準拠させないでください。
Sendable のチェック
次の場合に自動で行われます。
- アクターとの間でデータを受け渡す場合
- タスクとの間でデータを受け渡す場合
awaitでアクター境界を越える場合
func fetchAndDisplayImage(url: URL) async throws {
let (data, _) = try await URLSession.shared.data(from: url)
// ↑ Sendable ↑ Sendable (crosses to background)
let image = await decodeImage(data)
// ↑ data crosses to background (must be Sendable)
// ↑ image returns to main (must be Sendable)
}
よく使うパターン(コピーして使えるテンプレート)
パターン 1:Sendable な列挙型/構造体
使用場面:型がアクター境界を越える場合
// ✅ Enum (no associated values)
private enum PlaybackState: Sendable {
case stopped
case playing
case paused
}
// ✅ Struct (all properties Sendable)
struct Track: Sendable {
let id: String
let title: String
let artist: String?
}
// ✅ Enum with Sendable associated values
enum FetchResult: Sendable {
case success(data: Data)
case failure(error: Error) // Error is Sendable
}
パターン 2:デリゲートの値のキャプチャ(非常に重要)
使用場面:nonisolated なデリゲートメソッドから @MainActor の状態を更新する場合
nonisolated func delegate(_ param: SomeType) {
// ✅ Step 1: Capture delegate parameter values BEFORE Task
let value = param.value
let status = param.status
// ✅ Step 2: Task hop to MainActor
Task { @MainActor in
// ✅ Step 3: Safe to access self (we're on MainActor)
self.property = value
print("Status: \(status)")
}
}
理由:デリゲートメソッドは nonisolated です(ライブラリのスレッドから呼び出されます)。Task を開始する前にパラメーターをキャプチャしてください。Task { @MainActor in } 内で self にアクセスしても安全です。
パターン 3:タスク内での弱参照 Self
使用場面:タスクをプロパティとして保持する場合、または長時間実行する場合
class MusicPlayer {
private var progressTask: Task<Void, Never>?
func startMonitoring() {
progressTask = Task { [weak self] in // ✅ Weak capture
guard let self = self else { return }
while !Task.isCancelled {
await self.updateProgress()
}
}
}
deinit {
progressTask?.cancel()
}
}
注:短時間で終了するタスク(保持されないもの)では、強参照でキャプチャしてもかまいません。
パターン 4:@concurrent によるバックグラウンド処理
使用場面:CPU 負荷の高い処理を常にバックグラウンドで実行する場合(Swift 6.2 以降)
@concurrent
func decodeImage(_ data: Data) async -> Image {
// ✅ Always runs on background thread pool
// Good for: image processing, file I/O, JSON parsing
return Image()
}
// Usage
let image = await decodeImage(data) // Automatically offloads
要件:Swift 6.2 以降、Xcode 26 以降、iOS 26 以降
パターン 5:アクター分離されたプロトコル準拠(Swift 6.2 以降)
使用場面:特定のアクター分離を持つプロトコルに型を準拠させる場合
protocol Exportable {
func export()
}
class PhotoProcessor {
@MainActor
func exportAsPNG() {
// Export logic requiring UI access
}
}
// ✅ Conform with explicit isolation
extension StickerModel: @MainActor Exportable {
func export() {
photoProcessor.exportAsPNG() // ✅ Safe: both on MainActor
}
}
使用する場面:プロトコルのメソッドに特定のアクターコンテキストが必要な場合(UI にはメインアクター、処理にはバックグラウンド)
パターン 6:Sendable でない型のための #isolation キャプチャ(SE-0420)
使用場面:タスクのクロージャで Sendable でないデリゲートやオブジェクトを扱う場合
func process(
delegate: NonSendableDelegate,
isolation: isolated (any Actor)? = #isolation
) {
Task {
_ = isolation // Forces capture — Task inherits caller's isolation
delegate.doWork() // ✅ Safe: running on caller's actor
}
}
_ = isolation が必要な理由:SE-0420 によると、タスクのクロージャが分離されたパラメーターの非オプショナルなバインディングをキャプチャした場合にのみ、クロージャは分離を継承します。この行がないと、タスクはデフォルトのエグゼキューターで実行され、Sendable でないキャプチャがエラーになります。
使用する場面:Sendable でないデリゲートオブジェクトを扱うタスクの起動、呼び出し元の状態へのアクセスが必要な fire-and-forget の非同期処理、コールバックベースの API から非同期ストリームへのブリッジを行う場合。
パターン 7:タスクのタイムアウト
使用場面:非同期処理を期限内に完了する必要がある場合
func withTimeout<T: Sendable>(
_ duration: Duration,
operation: @Sendable @escaping () async throws -> T
) async throws -> T {
try await withThrowingTaskGroup(of: T.self) { group in
group.addTask { try await operation() }
group.addTask {
try await Task.sleep(for: duration)
throw TimeoutError()
}
guard let result = try await group.next() else {
throw TimeoutError()
}
group.cancelAll() // Cancel the loser (critical — without this it keeps running)
return result
}
}
// Usage
let data = try await withTimeout(.seconds(5)) {
try await slowNetworkRequest()
}
パターン 8:UI コードのための MainActor
使用場面:UI に触れるコード
@MainActor
class PlayerViewModel: ObservableObject {
@Published var currentTrack: Track?
@Published var isPlaying: Bool = false
func play(_ track: Track) async {
// Already on MainActor
self.currentTrack = track
self.isPlaying = true
}
}
データ永続化の並行処理パターン
パターン 9:SwiftData へのバックグラウンドアクセス
actor DataFetcher {
let modelContainer: ModelContainer
func fetchAllTracks() async throws -> [Track] {
let context = ModelContext(modelContainer)
let descriptor = FetchDescriptor<Track>(
sortBy: [SortDescriptor(\.title)]
)
return try context.fetch(descriptor)
}
}
@MainActor
class TrackViewModel: ObservableObject {
@Published var tracks: [Track] = []
func loadTracks() async {
let fetchedTracks = try await fetcher.fetchAllTracks()
self.tracks = fetchedTracks // Back on MainActor
}
}
パターン 10:Core Data のスレッドセーフなフェッチ
actor CoreDataFetcher {
func fetchTracksID(genre: String) async throws -> [String] {
let context = persistentContainer.newBackgroundContext()
var trackIDs: [String] = []
try await context.perform {
let request = NSFetchRequest<CDTrack>(entityName: "Track")
request.predicate = NSPredicate(format: "genre = %@", genre)
let results = try context.fetch(request)
trackIDs = results.map { $0.id } // Extract IDs before leaving context
}
return trackIDs // Lightweight, Sendable
}
}
パターン 11:進捗表示付きの一括インポート
actor DataImporter {
func importRecords(_ records: [RawRecord], onProgress: @Sendable @MainActor (Int, Int) -> Void) async throws {
let chunkSize = 1000
let context = ModelContext(modelContainer)
for (index, chunk) in records.chunked(into: chunkSize).enumerated() {
for record in chunk {
context.insert(Track(from: record))
}
try context.save()
let processed = (index + 1) * chunkSize
await onProgress(min(processed, records.count), records.count)
if Task.isCancelled { throw CancellationError() }
}
}
}
パターン 12:GRDB のバックグラウンド実行
actor DatabaseQueryExecutor {
let dbQueue: DatabaseQueue
func fetchUserWithPosts(userId: String) async throws -> (user: User, posts: [Post]) {
return try await dbQueue.read { db in
let user = try User.filter(Column("id") == userId).fetchOne(db)!
let posts = try Post
.filter(Column("userId") == userId)
.order(Column("createdAt").desc)
.limit(100)
.fetchAll(db)
return (user, posts)
}
}
}
簡易判断フローチャート
Starting new feature?
└─ Is UI responsive with all operations on main thread?
├─ YES → Stay single-threaded (Step 1)
└─ NO → Continue...
└─ Do you have high-latency operations? (network, file I/O)
├─ YES → Add async/await (Step 2)
└─ NO → Continue...
└─ Do you have CPU-intensive work? (Instruments shows main thread busy)
├─ YES → Add @concurrent or nonisolated (Step 3)
└─ NO → Continue...
└─ Is main actor contention causing slowdowns?
└─ YES → Extract subsystem to actor (Step 4)
Error: "Main actor-isolated property accessed from nonisolated context"
├─ In delegate method?
│ └─ Pattern 2: Value Capture Before Task
├─ In async function?
│ └─ Add @MainActor or call from Task { @MainActor in }
└─ In @concurrent function?
└─ Move access to caller, use await, or make nonisolated
Error: "Main actor-isolated property can not be referenced from a Sendable closure"
├─ Read-only access?
│ └─ Capture the value: { [count] in print(count) }
├─ Need mutation after background work?
│ └─ Extract background work to @concurrent func, call from @MainActor context:
│ func upload(_ data: Data) { Task { await doUpload(data); self.count += 1 } }
│ @concurrent func doUpload(_ data: Data) async { /* heavy work */ }
├─ Need mutation only?
│ └─ Task { @MainActor in self.count += 1 }
└─ Own the API?
└─ Make closure parameter @MainActor: (_ closure: @Sendable @MainActor () -> Void)
Error: "Capture of non-sendable type in @Sendable closure"
├─ Type is a struct?
│ └─ Add `: Sendable` (all stored properties must be Sendable)
├─ Type is a class you own?
│ └─ Convert to actor, or make final + immutable + Sendable
└─ Migration staging?
└─ @unchecked Sendable temporarily, track removal ticket
Error: "Value of non-Sendable type accessed after being transferred"
├─ Can avoid concurrent access?
│ └─ Capture as let: Task { [myArray] in ... }
├─ Need shared mutable access?
│ └─ Wrap in actor
└─ Redesign possible?
└─ Rework to avoid sharing mutable state across tasks
Error: "Reference to captured var in concurrently-executing code"
├─ Variable doesn't need to be mutable?
│ └─ Change var to let
├─ Needs to stay var?
│ └─ Capture in closure: { [task] in ... } or shadow: let t = task
└─ In a loop?
└─ Create let binding inside loop body before Task
Error: "Type does not conform to Sendable"
├─ Enum/struct with Sendable properties?
│ └─ Add `: Sendable`
└─ Class?
└─ Make @MainActor or keep non-Sendable (don't share concurrently)
Want to offload work to background?
├─ Always background (image processing)?
│ └─ Use @concurrent (Swift 6.2+)
├─ Caller decides?
│ └─ Use nonisolated
└─ Too much main actor state?
└─ Extract to actor
ビルド設定(Xcode 16 以降)
Build Settings → Swift Compiler — Language
→ "Default Actor Isolation" = Main Actor
→ "Approachable Concurrency" = Yes
Build Settings → Swift Compiler — Concurrency
→ "Strict Concurrency Checking" = Complete
Swift Package Manager での同等の設定:
.target(
name: "MyTarget",
swiftSettings: [
.swiftLanguageMode(.v6),
.defaultIsolation(MainActor.self)
]
)
これにより有効になる機能:
- メインアクターモード(デフォルトですべてのコードが @MainActor)
- コンパイル時のデータ競合防止
- 段階的な並行処理の導入
アンチパターンと誤った正当化
| 正当化 | 誤りである理由 | 代わりに行うこと |
|---|---|---|
「バックグラウンドで実行するために Task.detached を使います」 | Task.detached はアクターのコンテキスト、優先度、タスクローカル値を引き継ぎません。望んでいる動作であることはほとんどありません。 | バックグラウンドでの実行を強制するには @concurrent(Swift 6.2+)を使います。アクターを継承する必要がある場合は Task {} を使います。 |
「警告を黙らせるために @unchecked Sendable を追加します」 | データ競合をコンパイラーから隠すことになります。本番環境でクラッシュします。 | 型を本当に Sendable にする(struct/enum)、アクターを使う、または sending パラメーターを使います。 |
「これを直すために nonisolated(unsafe) を使います」 | 実行時の保護は一切ありません。コンパイラーによるチェックが止まり、データ競合が検出されなくなります。 | 適切な隔離(@MainActor、アクター、Mutex)を使います。グローバル定数に限って使用します。 |
| 「この非同期関数はバックグラウンドスレッドで動きます」 | async はブロックせずにサスペンドしますが、同じアクター上で再開します。@MainActor の非同期関数はメインスレッドで実行されます。 | バックグラウンドでの実行を強制するには @concurrent を使います。async だからバックグラウンドで動くと決めつけないでください。 |
「これを DispatchQueue.global().async で囲みます」 | 構造化並行処理の中で GCD によるキュー切り替えを行うと、隔離の保証が損なわれ、スレッドが過剰に増えるリスクがあります。 | @concurrent を使うか、処理をアクターに切り出します。GCD はブリッジ層だけで使います。 |
| 「すべてのクラスをアクターにする必要があります」 | アクターは直列化のオーバーヘッドを増やします。カスタムアクター上の UI コードではビューを更新できません。 | UI/ViewModel コードには @MainActor を使います。アクターを使うのは、UI に関係しない共有可変状態に限ります。 |
「より早くリリースするために @preconcurrency を使います」 | コンパイラーが検証できないスレッドセーフ性を前提にしています。本番環境でクラッシュが発生します。 | 適切な並行処理へ移行します。@preconcurrency は削除チケットを作成したうえで、一時的なブリッジとしてのみ使います。 |
| 「この機能には並行処理が必要です」 | 並行処理は複雑さを増します。ほとんどのアプリコードは MainActor 上の単一スレッドで問題なく動作します。 | まずプロファイリングします。Instruments でボトルネックが確認された場合にのみ、段階的に対応します。 |
| 「各 Task はスレッドに対応します」 | タスクは協調型スレッドプールを共有します。サスペンド後、タスクは任意のスレッドで再開される可能性があります。 | スレッドではなく、隔離ドメインを基準に考えます。 |
| 「作業の各項目に対して Task を生成します」 | 非構造化 Task ではキャンセルの伝播とエラー処理が失われます。動作の把握も難しくなります。 | 関連する並列処理には、async let(固定数)または TaskGroup(動的な数)を使います。Task {} は同期処理から非同期処理へのブリッジに限って使います。 |
❌ 不要な場面で並行処理を使う
// ❌ Premature optimization
@concurrent
func addNumbers(_ a: Int, _ b: Int) async -> Int {
return a + b // ❌ Trivial work, concurrency adds overhead
}
// ✅ Keep simple
func addNumbers(_ a: Int, _ b: Int) -> Int {
return a + b
}
❌ 保存された Task で強参照する self
// ❌ Memory leak — task retains self, self retains task
progressTask = Task {
while true {
await self.update() // ❌ Strong capture in infinite loop
}
}
// ✅ Weak capture breaks the cycle
progressTask = Task { [weak self] in
while let self, !Task.isCancelled {
await self.updateProgress()
}
}
目安:短時間で完了する Task であれば、self の強参照は問題ありません。Task をプロパティとして保存する場合や、無期限に実行する場合(ループ、非同期シーケンス)は [weak self] を使います。
❌ すべてのクラスをアクターにする
// ❌ Don't do this
actor MyViewModel: ObservableObject { // ❌ UI code should be @MainActor!
@Published var state: State // ❌ Won't work correctly
}
// ✅ Do this
@MainActor
class MyViewModel: ObservableObject {
@Published var state: State
}
❌ 非同期コンテキストでの Thread.current
// ❌ Swift 6 language mode: compiler error
func checkThread() async {
print(Thread.current) // ERROR: unavailable from asynchronous contexts
}
// ✅ If you must inspect threads (debugging only)
extension Thread {
static var currentThread: Thread { Thread.current }
}
print(Thread.currentThread) // Works, but don't rely on thread identity
理由:サスペンド時にタスクはスレッド間を移動します。Swift Concurrency ではスレッドの識別に意味はありません。代わりに、隔離ドメインを基準に考えます。
コードレビューのチェックリスト
並行処理を追加する前に
- プロファイリングを行い、UI の応答性が低下していることを確認した
- 具体的な低速処理(ネットワーク、CPU、競合)を特定した
- 最もシンプルな解決策から始めた(async → concurrent → アクター)
Async/Await
- 高レイテンシーの処理にのみ使用している
- イベントへの応答として Task を作成している
- do-catch でエラーを処理している
バックグラウンド処理
- 常にバックグラウンドで実行する処理には
@concurrentを使っている(Swift 6.2+) - ライブラリー API には
nonisolatedを使っている - メインアクター上でブロッキング処理を行っていない
Sendable
- アクター間で受け渡すデータには値型を使っている
- クラスは @MainActor または非 Sendable のままにしている
- 共有クラスを並行して変更していない
アクター
- UI に関係しないサブシステムにのみ使っている
- UI コードは @MainActor 上にある
- モデルクラスは @MainActor または非 Sendable のままにしている
移行の習慣
既存のコードベースを厳密な並行処理に移行する際は、次の手順に従ってください。
- 小さな単位で繰り返す — 一度に修正するのは、モジュールひとつまたは診断カテゴリひとつにします。再ビルドし、テストして、コミットします。まとめて変更しないでください。
- 新しい型は最初から Sendable として設計する — 既存の型に後から Sendable を適用すると、コードベース全体に影響が広がります。
- 早い段階でデフォルトの分離を設定する — アプリのモジュールでは、デフォルトの分離を
@MainActorに設定します。これにより、誤検知の警告の大半を解消できます。 - スコープの拡大を避ける — 並行処理への移行を目的とする PR には、並行処理に関する変更のみを含め、アーキテクチャのリファクタリングを含めないでください。
- リリースを急ぐために警告を抑制しない —
@unchecked Sendable、nonisolated(unsafe)、@preconcurrencyは移行のための手段であり、恒久的な解決策ではありません。それぞれの削除チケットを登録してください。
リソース
WWDC:2025-268、2025-245、2022-110351、2021-10133
ドキュメント:/swift/adoptingswift6、/swift/sendable
スキル:axiom-lldb(デバッガーでアクター/タスクの状態をデバッグ)
最終更新日:2025-12-01
ステータス:WWDC 2025-268 の段階的な移行、@concurrent 属性、分離された適合性、取り組みやすい並行処理パターンを追加して強化