ハイライト
Xcode 26 で新規 App プロジェクトを作成すると、デフォルトで Main Actor モードが有効になり、すべてのコードが暗黙的に
@MainActorを帯びるようになります。バックグラウンドで実行したい場合は@concurrentを明示的に付与します。
主要内容
ネットワークリクエストを書き始めた頃、多くの開発者が同じ落とし穴に踏み込んだ経験があるはずです。メインスレッドで URLSession.shared.data(from:) を同期的に呼び出した結果、UI が数秒間フリーズし、アプリが固まったように見えてしまう、というあれです。これは、メインスレッドがタッチイベントへの応答と UI の描画を担っているのに、ネットワーク IO に占有されてしまうと何もできなくなるためです。今回 Swift チームが提示した解決策は、新たな GCD API を追加するのではなく、「どのスレッドで実行するか」を言語レベルでコンパイラが検証できるプロパティに変えるというものです。
iOS 19 / Xcode 26 で新規作成した App プロジェクトでは、デフォルトで Main Actor モードが有効になっています。これはつまり、何のアノテーションも付けないコードが暗黙的に @MainActor を帯びる、ということです。コンパイラはそのコードがメインスレッドでしか動かないと知っているので、グローバル変数や singleton にも自由にアクセスできます。ある計算(たとえば画像のデコード)が UI のフレーム落ちを引き起こすようになったら、その関数に @concurrent を付けます。すると、コンパイラがメインスレッド専有の状態にアクセスしている箇所をすぐに指摘し、それらの状態を値型もしくは actor で包んでから渡すように促してくれます。マイグレーションのパスは段階的です。シングルスレッドから始めて、まず async/await で高レイテンシ処理を扱い、次に @concurrent で CPU バウンドなタスクをオフロードし、最後に actor でデータ自体をメインスレッドの外に追い出す、という流れになります。
詳細
セッション全体は、画像読み込みのサンプルを軸に展開していきます。最初のバージョンは同期ブロッキングです。
final class ImageModel {
var imageCache: [URL: Image] = [:]
let view = View()
func fetchAndDisplayImage(url: URL) throws {
let data = try Data(contentsOf: url)
let image = decodeImage(data)
view.displayImage(image)
}
}
ポイント:
fetchAndDisplayImageは同期関数で、処理全体がメインスレッド上で一気に走ります。- ローカルの小さなファイルを読むぶんには問題ありませんが、ネットワーク URL に切り替えた途端、メインスレッドがネットワーク IO でブロックされ、UI が完全に固まります。
最初のステップは async 化して、ネットワーク IO がメインスレッドをブロックしないようにすることです(06:10)。
final class ImageModel {
var imageCache: [URL: Image] = [:]
let view = View()
func fetchAndDisplayImage(url: URL) async throws {
let (data, _) = try await URLSession.shared.data(from: url)
let image = decodeImage(data)
view.displayImage(image)
}
}
ポイント:
- 関数シグネチャに
asyncを付け、URLSessionを呼び出すときはawaitを使います。 awaitは suspend point を示し、関数はここでメインスレッドを手放し、データが返ってきてから再開します。- 関数自体は依然としてメインスレッド上で動いていますが、ネットワークを待っている間、メインスレッドはタッチイベントの処理や別の UI の描画に使えます。
URLSessionの実装はバックグラウンドスレッドで実際の IO を行うため、アプリのコード自体には並行性が持ち込まれていません。
async 関数は task の中で起動する必要があります。ボタンタップ時のコードはこうなります(07:31)。
func onTapEvent() {
Task {
do {
try await fetchAndDisplayImage(url: url)
} catch let error {
displayError(error)
}
}
}
ポイント:
Task { ... }で新しい task を作り、fetch + display 全体をラップします。- task は最初から最後まで順番に実行され、複数の task はメインスレッド上で interleaving しながら交互に走ります。
- error は task の内部で catch して、例外を握りつぶさないようにします。
実際にメインスレッドの詰まりに遭遇したとき——たとえば decodeImage で大きな画像を扱うのが遅すぎるとき——に初めて、その部分をバックグラウンドへ移します。ここで使うのが @concurrent です(11:11)。
final class ImageModel {
var imageCache: [URL: Image] = [:]
let view = View()
func fetchAndDisplayImage(url: URL) async throws {
let (data, _) = try await URLSession.shared.data(from: url)
let image = await decodeImage(data, at: url)
view.displayImage(image)
}
@concurrent
func decodeImage(_ data: Data, at url: URL) async -> Image {
Image()
}
}
ポイント:
@concurrentは、この関数が常にバックグラウンドで動くことを Swift に伝えます。- 呼び出し側は
awaitで結果を待ち、デコード中のメインスレッドは別の処理を続けられます。 - この時点で
decodeImageは main actor の隔離ドメインから外れるため、コンパイラはimageCacheへの直接アクセスを拒否します。これは設計上の意図であり、スレッドをまたいで共有するデータを明示的に扱うように強制しています。
共有可変参照型におけるデータ競合の問題に対しては、actor を使った解決策が示されます(25:10)。
actor NetworkManager {
var openConnections: [URL: Connection] = [:]
func openConnection(for url: URL) async -> Connection {
if let connection = openConnections[url] {
return connection
}
let connection = Connection()
openConnections[url] = connection
return connection
}
func closeConnection(_ connection: Connection, for url: URL) async {
openConnections.removeValue(forKey: url)
}
}
ポイント:
classをactorに変えるだけで、openConnectionsの可変状態が actor によって隔離・保護されます。- 外部から
openConnection/closeConnectionを呼ぶときはawaitが必須となり、actor は同時に内部で動く呼び出しが 1 つだけになることを保証します。 - メインスレッドからもバックグラウンドスレッドからも、同一の
NetworkManagerインスタンスを安全に利用でき、ロックを手で書く必要がありません。
値型は別ルートを通ります。struct と enum はデフォルトで Sendable を満たし、task 間で受け渡すときはコピーセマンティクスが共有を自動的に防いでくれます(16:56)。まず値型を試し、共有可変状態をどうしても扱えないときに actor を使う、というのが Apple の推奨する優先順位です。
重要ポイント
-
やるべきこと:新規 iOS 19 プロジェクトでは Main Actor デフォルトモードが有効になっていることを確認する
- なぜやる価値があるか:Xcode 26 の新テンプレートではデフォルトで有効化されており、コンパイラが UI モジュールのコードに自動で
@MainActorを付けてくれます。明示的なアノテーションを大量に書く必要がなくなり、UI 関連の singleton やグローバル状態へのアクセスで「data race」と怒られることもなくなります。 - どこから始めるか:Build Settings で「Default Actor Isolation」を検索し、
MainActorになっていることを確認します。既存プロジェクトでは UI 関連のモジュールから切り替え、ビジネスロジックのモジュールはいったん現状維持で構いません。
- なぜやる価値があるか:Xcode 26 の新テンプレートではデフォルトで有効化されており、コンパイラが UI モジュールのコードに自動で
-
やるべきこと:
URLSession.shared.data(...)の同期呼び出しをすべてasync/awaitに置き換える- なぜやる価値があるか:メインスレッド上での同期ネットワーク IO は、ほぼ確実に UI のフリーズを招きます。
async版を使えば並行性を持ち込む必要はなく、awaitを 1 つ書くだけでフリーズ箇所が suspend point に変わります。 - どこから始めるか:コード内のすべての
Data(contentsOf:)や同期版URLSessionの使用箇所を検索し、呼び出し元をasync関数に書き換え、ボタンのコールバックの中でTask { }を使って起動するようにします。
- なぜやる価値があるか:メインスレッド上での同期ネットワーク IO は、ほぼ確実に UI のフリーズを招きます。
-
やるべきこと:Instruments でメインスレッド上の本当に重い計算を特定してから
@concurrentを付与する- なぜやる価値があるか:セッションでも「多くの app は並行性を必要としない」と明言されています。重そうな処理を闇雲にバックグラウンドへ追い出しても、コードが複雑化し、新たなデータ競合を呼び込むだけです。まず profiler でボトルネックを突き止めるべきです。
- どこから始めるか:Instruments の Time Profiler を実行し、メインスレッド占有率の高い関数を見つけます。それが本当に CPU バウンドな処理(画像デコード、大きな JSON のパースなど)であれば、その関数に
@concurrentを付けて、コンパイラに隔離状態へのアクセス箇所を洗い出してもらいます。
-
やるべきこと:スレッド間で共有される可変参照型を actor にリファクタリングする
- なぜやる価値があるか:singleton 的な
classは複数スレッドからアクセスされるとロックを手で書く必要があり、ミスを誘発します。actorは同期プリミティブを言語レベルで内蔵し、外部からの呼び出しはawaitが必須になるため、コンパイラがすべてのデータ競合を未然に止めてくれます。 - どこから始めるか:プロジェクト内で複数スレッドからアクセスされる manager / cache 系のクラスを洗い出し、順番に
actorへ書き換えていきます。外部からの呼び出しはすべてawaitが必要になるため、呼び出しチェーン上の関数もasync化が必要になる場合があります。
- なぜやる価値があるか:singleton 的な
-
やるべきこと:値型 +
Sendableで task 間のデータ受け渡しを処理する- なぜやる価値があるか:
structはデフォルトでSendableを満たし、コピーセマンティクスが共有を自然に避けてくれるため、actor よりずっとシンプルです。Apple もセッション内で「値型優先」を第一原則として挙げています。 - どこから始めるか:task 間で受け渡しているデータを見直し、
structで表現できるならclassは使わない方針にします。参照型を渡さざるを得ない場合は、それが不変であることを確認したうえでSendableを付け、そうでなければ値型のスナップショットに変換します。
- なぜやる価値があるか:
関連セッション
- Code-along: Cook up a rich text experience in SwiftUI with AttributedString — SwiftUI でリッチテキスト編集体験を組み立てつつ、Swift concurrency でデータ更新を扱う
- Explore Swift and Java interoperability — Swift と Java を同一コードベースで併用する際、言語をまたぐ呼び出しで並行性の境界をどう扱うか
- Improve memory usage and performance with Swift — Swift のメモリ管理とパフォーマンス最適化のテクニック。
@concurrentでのオフロードと組み合わせて読むのに最適 - Optimize SwiftUI performance with Instruments — 新しい SwiftUI Instrument で UI のフリーズを特定し、並行性を導入すべきかを判断する
コメント
GitHub Issues · utterances