Highlight
Swift 6.2 では並行モデルが「デフォルト並行」から「デフォルトシングルスレッド」へと方針転換されました。あわせて
InlineArray、Span、Subprocess、Observationsといった基盤ライブラリが追加され、macros プロジェクトでは swift-syntax のプリビルド化により clean build 時間が大幅に短縮されています。
主な内容
Swift 6 の並行コードをある程度書いてきた方であれば、誰もが似たようなエラーに悩まされた経験があるはずです。普通の class に async メソッドを 1 つ足しただけで、コンパイラは即座に main actor の境界をまたいでいると警告します。@MainActor な型に普通のプロトコルを準拠させようとすると、今度はプロトコル実装がスレッドをまたぐ可能性があると指摘されます。アプリでもっとも一般的な構成、つまり UI 型・シングルトン・コールバックがすべて並行安全性チェックの網に引っかかってしまうわけです。結果として開発者は、あちこちに @MainActor を貼りまくるか、本人もよく理解できないような形にコードを作り変えるかを迫られてきました。
Swift 6.2 はこの問題を根本から考え直しました。新しいデフォルトはこうです。async 関数は呼び出し側の actor 上で実行される、@MainActor をモジュール全体のデフォルト推論モードとして有効化できる、プロトコル準拠を @MainActor Exportable のように明示的に注釈することで main actor の型でも合法的にプロトコルへ準拠できる。完全にメインスレッドで動くアプリは、Swift 5 時代のような直感的な書き方に戻りつつ、Swift 6 のデータ競合チェックは引き続き有効なままです。CPU 負荷の高い処理をバックグラウンドへオフロードしたい場合は、@concurrent で一度だけ明示的に注釈すれば、意図が明確になり、性能特性も予測しやすくなります。
並行性以外にも、6.2 では日々お世話になっている多くの API が刷新されました。NotificationCenter には通知ごとに具体的な型が与えられ、辞書からの値取得やキャストが不要になります。新しい Subprocess パッケージを使えば、スクリプト的な用途で await run(.name("pwd")) の一行だけでプロセスを起動できます。Observations は @Observable の状態変化を AsyncSequence としてラップしてくれます。Swift Testing には Attachment と exit tests が加わりました。ツールチェイン側でも、swiftly 1.0 が macOS でリリースされ、プリビルド化された swift-syntax のおかげで macros プロジェクトの clean build 時間が数分単位で短縮されます。Explicitly Built Modules により、LLDB で初めて p / po を実行したときのもたつきも解消されています。
詳細内容
新しい Subprocess パッケージ(09:44)。Foundation Workgroup は、スクリプト系の用途でもっとも頻出する「サブプロセスの起動」を独立した package として切り出しました。
import Subprocess
let swiftPath = FilePath("/usr/bin/swift")
let result = try await run(
.path(swiftPath),
arguments: ["--version"]
)
let swiftVersion = result.standardOutput
ポイントは次のとおりです。
import Subprocess: 独立した swift-subprocess パッケージから提供されます。現状は 0.1 版で、1.0 に向けてフィードバックを募集中です。.path(swiftPath):FilePathで実行ファイルのフルパスを指定します。文字列を渡した場合は$PATHから検索されます。try await run(...): 戻り値のresultには exit status、standardOutput、standardErrorなどが含まれており、async/await モデルと自然に噛み合います。
NotificationCenter が具体的な型を持つ(11:34)。これまでキーボード通知を観察するには、UIResponder.keyboardWillShowNotification という文字列を渡し、userInfo 辞書から CGRect を取り出し、main actor の隔離まで自分で扱う必要がありました。新しい API では、これらをすべて 1 行に集約できます。
import UIKit
@MainActor
class KeyboardObserver {
func registerObserver(screen: UIScreen) {
let center = NotificationCenter.default
let token = center.addObserver(
of: screen,
for: .keyboardWillShow
) { keyboardState in
let startFrame = keyboardState.startFrame
let endFrame = keyboardState.endFrame
self.keyboardWillShow(startFrame: startFrame, endFrame: endFrame)
}
}
func keyboardWillShow(startFrame: CGRect, endFrame: CGRect) {}
}
ポイントは次のとおりです。
for: .keyboardWillShow:UIScreenが本当にこの通知を発行するかどうかをコンパイル時に検査します。通知名のタイポは即コンパイルエラーになります。keyboardState.startFrame: payload は強く型付けされた構造体になり、辞書の key とas? CGRectの組み合わせが不要になります。- 通知は
NotificationCenter.MainActorMessageとして宣言されており、コールバックはそのまま main actor 上で実行されるため、MainActor.assumeIsolatedのような追加処理は不要です。
Isolated conformances によるプロトコルの境界またぎ問題の解消(33:04)。これまで @MainActor 型が普通のプロトコルに準拠しようとすると “crosses into main actor-isolated code” というエラーが出ていました。Swift 6.2 では、プロトコル準拠そのものに actor 注釈を書けるようになります。
protocol Exportable {
func export()
}
extension StickerModel: @MainActor Exportable {
func export() {
photoProcessor.exportAsPNG()
}
}
ポイントは次のとおりです。
@MainActor Exportable: この conformance が main actor のコンテキストでのみ利用可能であることを宣言します。- 呼び出し側も自分自身が main actor 上で動いている必要があります。そうでなければコンパイルエラーになり、境界またぎのチェックは「準拠そのものを禁止する」から「使用時に検査する」へと緩和されました。
- モジュール全体の
@MainActorデフォルト推論モードと組み合わせれば、シングルスレッドのアプリで自前の@MainActor注釈を書く機会はほぼなくなります。
バックグラウンドへの明示的なオフロード(35:06)。新しいモデルでは async は呼び出し側の actor 上で動くのがデフォルトとなったため、グローバル並行 executor へ切り替えたい場合は明示的な注釈が必要です。
class PhotoProcessor {
var cachedStickers: [String: Sticker]
func extractSticker(data: Data, with id: String) async -> Sticker {
if let sticker = cachedStickers[id] {
return sticker
}
let sticker = await Self.extractSubject(from: data)
cachedStickers[id] = sticker
return sticker
}
@concurrent
static func extractSubject(from data: Data) async -> Sticker {}
}
ポイントは次のとおりです。
extractStickerには@concurrentが付いていないので、呼び出し側の actor 上で動作し、キャッシュの読み書きは自然にスレッドセーフです。@concurrent static func extractSubject: 並行実行可能と注釈されており、main actor を離れてグローバル executor で実行されます。- CPU 負荷の高い処理での actor hop を 1 か所に集中させられるため、性能チューニングの際にも数少ない
@concurrent関数だけを見ればよくなります。
Observations で状態変化を AsyncSequence に(14:05)。
let player = Player(name: "Holly")
let values = Observations {
let score = "\(player.score) points"
let item =
switch player.item {
case .none: "no item"
case .banana: "a banana"
case .star: "a star"
}
return "\(score) and \(item)"
}
player.score += 2
player.item = .banana
for await value in values { print(value) }
ポイントは次のとおりです。
Observations { ... }: クロージャ内でアクセスしたすべての@Observableプロパティが自動的に追跡対象になります。- 同じ tick 内で複数回書き込みが行われた場合(
score += 2とitem = .banana)、それらは 1 回のトランザクション的な更新としてまとめられ、中間状態が観測されることはありません。 for await value in values: UI 状態をAsyncSequenceとして駆動できるようになり、SwiftUI 以外の非同期ループとも自然に統合できます。
重要ポイント
1. Xcode の build settings で @MainActor のデフォルト推論モードを有効にする
なぜ取り組む価値があるか: シングルスレッドの UI アプリは大多数の業務コードのあり方そのものです。このモードを有効にすれば、画面いっぱいに散らばる @MainActor 注釈を省略でき、Swift 5 並みの可読性を保ちつつ、Swift 6 のデータ競合チェックは維持されます。
どう始めるか: SwiftPM manifest の SwiftSettings API でまとめて有効化すれば、ファイル単位で書き換える手間を避けられます。新規プロジェクトはデフォルトで有効になります。
2. 性能クリティカルなパスの Array を InlineArray に置き換える
なぜ取り組む価値があるか: サイズが固定のデータ(カラーマトリクス、ルックアップテーブル、グリッドの状態など)は、InlineArray を使うことでスタックにインラインで保持でき、ヒープアロケーションと参照カウントを削減できます。レンダリング・デコード・信号処理のパスでは目に見える効果が得られることが多いです。
どう始めるか: まず Instruments で hot path のメモリアロケーションホットスポットを特定し、それから 1 か所ずつ置き換えていきます。安全なゼロコピービューが必要な場面では Span と組み合わせます。
3. NotificationCenter の観察を具体的な型 API へ移行する
なぜ取り組む価値があるか: 「通知名のタイポ」「userInfo key のタイポ」「main actor の境界またぎ」という頻出する 3 種類のバグを排除でき、呼び出し側のコード行数も半減します。型そのものがドキュメントとして機能するようになります。
どう始めるか: UIKit のキーボード通知やアプリのライフサイクル通知など、Apple 側で既に移行済みのものから着手します。独自通知についても MainActorMessage / AsyncMessage プロトコルと具体的な payload 型を組み合わせて定義します。
4. macros プロジェクトに pre-built swift-syntax を組み込む
なぜ取り組む価値があるか: CI の clean build を毎回数分短縮でき、ローカル Xcode の初回ビルドも体感で高速化します。macro の作者が tagged release を公開すれば、下流のユーザは何も変更しなくても恩恵を受けられます。
どう始めるか: Xcode 26 / Swift 6.2 にアップデートし、macro パッケージの依存を swift-syntax の tagged release に固定したうえで、build log にプリビルド成果物がヒットしているかを確認します。
5. あちこちに散らばった Task.detached を @concurrent で置き換える
なぜ取り組む価値があるか: 「この処理は現在の actor を離れる」という意図を呼び出し点ではなく関数シグネチャに刻めるため、レビューしやすくなり、性能分析の境界も明確になります。
どう始めるか: 既存の Task.detached や DispatchQueue.global().async を見直し、静的関数で表現できるものは @concurrent に置き換えます。実行時にスケジューリングが必須となる少数のケースだけは残しておきます。
関連 Session
- Code-along: Elevate an app with Swift concurrency — コードに沿って手を動かしながら、既存アプリを Swift 6.2 並行モデルへアップグレードする
- Embracing Swift concurrency — デフォルトシングルスレッドや isolated conformance といった設計判断を体系的に解説
- Explore Swift performance — InlineArray、Span、メモリ安全性の底層メカニズムを掘り下げる
- What’s new in Xcode 26 — Explicitly Built Modules やデバッグ改善などツールチェインの変化
- Meet Containerization — Apple が Swift で実装したオープンソースのコンテナランタイム
コメント
GitHub Issues · utterances