write-swift について
write-swiftは、Swift開発者向けのプログラミングガイドスキルです。モダンなSwiftのデータ設計、エラー処理、並行処理を扱い、複雑な仕組みは必要に応じて段階的に導入する方針です。
主な機能
- 01値型とデータモデリングの指針
- 02エラーとオプショナルの処理
- 03Swiftの並行処理と隔離ルール
- 04Sendableと隔離領域間の共有指針
- 05構造化並行処理の指針
活用シーン
- 01Swiftのデータにstruct、enum、classを選択
- 02エラー処理とオプショナルの流れを確認
- 03プロファイリングに基づく並行処理の計画
- 04データ競合やSendableの問題を調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ emilkowalski/ skills --skill write-swiftインストール後、Claude Code で/write-swiftを入力して読み込まれたか確認します。
詳細
Write Swift
初回の応答
このスキルが特定の質問なしで最初に呼び出されたときは、次のようにだけ応答してください:
I'm ready to help you write modern Swift, the way the language wants to be written.
ユーザーが質問するまで、これ以外の情報は提供しないでください。
言語が望む書き方で Swift を書く方法。Swift 6.4 まで対応しています。
ツールチェーンのベースライン:Swift 6.3(2026 年 8 月時点の最新リリース)。ここに記載されている内容は、⚠ が付いているものを除き、すべて 6.3 でコンパイルできます。⚠ は未リリースの Swift 6.4 の機能を示します。並行処理のガイダンスは Swift 6.2 のモデルを前提としています。プロジェクトが 6.1 以前の場合、§3 の async と @concurrent に関するルールは適用されません。
一貫するテーマ:Swift は段階的開示(progressive disclosure)の言語です。最もシンプルで、最も静的で、最もシングルスレッドな、動作するものから始め、動的なもの — 並行処理、参照セマンティクス、existential、安全でないポインタ — は、その理由を指し示せるところでのみ手に入れてください。 以下のすべてのルールは、この原則の適用例です。
このデフォルトの階層をモデル化してください。一段下へ移動するのは、言明できる理由があるときだけです:
| 必要性 | 選ぶもの | 一段下へ移動するのは |
|---|---|---|
| データ | struct / enum | アイデンティティ、共有、継承が必要なとき |
| 抽象化 | 具象型 | 型をまたいでコードが繰り返されているとき |
| ポリモーフィズム | some P(ジェネリクス) | 異種の格納が必要なとき → any P |
| 実行 | メインアクター、同期 | プロファイリングでハングが判明したとき → async → @concurrent → actor |
| メモリ | Array、String | プロファイリングでコストが判明したとき → InlineArray、Span |
| 安全性 | 安全な API | C 相互運用、または計測されたホットパス → Unsafe* |
1. 値型でデータをモデリングする
値型は Swift では特別なケースではなく、デフォルトです。
- デフォルトでは
structとenumを使います。classは同一性、共有された可変状態、継承、リソースのライフタイムにのみ使用します。 ウィンドウ、データベース接続、レンダリングエンジンに格納されたエンティティ — これらには同一性があります。Point、Drink、Materialにはありません。 - デフォルトは
let、変更する場合のみvar。 これはanyより先にsome、参照より先に値、と同じ規律です。狭く始め、理由があるときに広げます。 - 可変の参照型プロパティを持つ構造体は、値でも参照でもありません。 コピーはオブジェクトを共有し、変更はコピー間で漏れ出します。参照型をイミュータブルに保つか、それに転送する計算プロパティのみを公開するか、コピーオンライトの背後で
privateのストアドプロパティにするかのいずれかです。 - コピーオンライトは、アウトオブラインのストレージ と 値セマンティクスの両方を得る方法です。 final クラスを構造体でラップし、変更前に
isKnownUniquelyReferenced(&storage)をチェックします。そうでなければ先にコピーします。これはまさにArray、String、Dictionaryが動作する仕組みです。 - enum は「固定された一連のもの」と相互排他的な状態のためのツールです。 オプショナルのストアドプロパティの山(
isSharing、selectedRows、shareTarget)を 1 つのenum Stateに置き換えることで、無効な組み合わせを表現不可能にし、状態変更を、完了し忘れる可能性のある一連のプロパティ書き込みではなくアトミックにします。 - 値を合成すると値が得られます。 ストアドプロパティがすべて値型である構造体は、値セマンティクスを自動的に得ます。これにより、アンドゥ、差分、状態の復元が、プロパティごとに 1 つではなく単一のコードパスになります。
struct Material { // value semantics preserved
var roughness: Double
private var _texture: Texture // a class
var color: Color {
get { _texture.color }
set {
if !isKnownUniquelyReferenced(&_texture) { _texture = Texture(copying: _texture) }
_texture.color = newValue
}
}
}
非コピー可能型(~Copyable)は一意な所有権を表現します。ファイルディスクリプタ、銀行振込、オープン中のリソースなどです。コピーを抑制することで、「これを 2 回実行してはならない」という制約がアサーションからコンパイルエラーになり、構造体の deinit が意味を持ちます。終了メソッドを consuming とマークすることで、コンパイラがそれが最後の使用であることを証明します。パラメータの所有権が明示的になります。borrowing(読み取り専用、デフォルト)、consuming(奪い取る)、inout/mutating(一時的な書き込みアクセス)です。
2. エラーとオプショナル — 失敗パスを可視化する
Swift のエラー処理は 3 つの点に基づいています。エラーの発生源はマークされ、予期せぬ事態を招かないこと。エラーは対処するのに十分なコンテキストを持つこと。そして 回復可能なエラーはプログラマのミスとは異なる ことです。
- 回復可能 →
throw。プログラマのミス →precondition/fatalError。 失敗したネットワーク呼び出しはプログラムを動かし続けます。範囲外のインデックスはコードが間違っていることを意味し、バグがセキュリティ問題になる前に停止しなければなりません。 - 関連値を持つ enum が最良のエラー型です。
case duplicateFriend(String)はcase duplicateFriendに勝ります。コンテキストが要点だからです。 - エラー条件には
guardを使います。終了パスを強制するからです。通常のアンラップにはif letを使います。 - 型付き throws(
throws(MyError))は、内部関数、エラー転送のジェネリックコード、およびany Errorのボクシングが高コストすぎる制約のある環境 に適しています。公開 API では、型なしのthrowsを使うことで、後でエラー型を変更する自由が保たれます。統一に注意してください。throwsはthrows(any Error)であり、非スローはthrows(Never)です。これによりmapが両方を抽象化できます。 - 強制アンラップは不変条件を明示できる場合にのみ 行い、素の
!よりもメッセージ付きの失敗する#require/preconditionを優先します。
3. 並行性: プロファイリングが示すまではシングルスレッドのままにする
これはエージェントが最もよく間違えるセクションです。なぜなら、Swift 6.2 でモデルが変わったからです。
すべてのアプリを完全にメインスレッドで開始してください。シングルスレッドのコードは非常に広い範囲をカバーでき、ほとんどのアプリはそこから離れる必要がまったくありません。
順序立てた段階的な進行。ステップを飛ばさないでください。
- メインアクター上でのシングルスレッド。 並行性は一切なし。ほとんどのアプリでこれで十分です。
async/awaitでレイテンシ(ネットワーク、ディスク)を隠蔽します。依然として自分自身の並行性はありません —URLSession.data(from:)のような SDK API があなたの代わりに処理をオフロードします。@concurrentで 自分の 高コストな処理をメインスレッドから追い出します — Instruments がハングを示した後にのみ行います。actorで 状態 をメインアクターから移動します — メインアクター上の状態が多すぎてタスクが絶えずホップバックすることを強いられている場合にのみ行います。
まず適切なビルド設定をオンにしてください。 すべてのプロジェクトで Approachable Concurrency を有効にします。アプリモジュールと UI に面するモジュールでは、Default Actor Isolation を MainActor に設定してください — これは Xcode 26 の新規アプリプロジェクトのデフォルトであり、@MainActor 注釈のほとんどを削除できます。パッケージ内では: swiftSettings: [.defaultIsolation(MainActor.self)]。汎用ライブラリでは main-actor-by-default を設定しないでください — ライブラリは nonisolated API を提供し、処理をどこで実行するかはクライアントが決めるべきです。
変わったルール
Swift 6.2 では、関数を async とマークしても、その関数は現在のアクターから移動しません。 呼び出された場所で実行されます。これにより「書くのが最も自然なコード」がデフォルトでデータ競合フリーになります。
@concurrent— 常に並行スレッドプールに切り替えます。自分の CPU 負荷の高い処理に使用してください。nonisolated— 呼び出された場所で実行されます。これがライブラリ API の適切なデフォルトです、呼び出し側が決定するからです。型にnonisolatedを付けると、そのすべてのメンバーが nonisolated になります(Swift 6.1 以降)。- どちらでもない場合 — 呼び出し元のアクター上にとどまります。
nonisolated struct PhotoProcessor { // decoupled from the main actor
@concurrent // guaranteed to run in the background
func process(_ data: Data) async -> ProcessedPhoto {
async let sticker = extractSticker(data) // two independent jobs, in parallel
async let colors = extractColors(data)
return await ProcessedPhoto(sticker: sticker, colors: colors)
}
}
- オフロードする前にプロファイリングする。 Instruments(Time Profiler、ハング)を使用してください。並行性なしでコードを高速化できるなら、常にそれを最初に行ってください。並行性には実際のコストが伴います — タスクの割り当て、スケジューリング、そして推論です。
- 些細な処理のためにタスクを生成しない。
UserDefaultsの値を読むための子タスクは、節約できる以上のコストがかかります。 - エンドツーエンドの操作ごとに 1 つのタスク。 順序通りに実行する必要がある処理は 1 つの タスクに入れ、独立した操作は別々のタスクにして、ランタイムがそれらをインターリーブできるようにします。
awaitはサスペンションポイントであり、原子性を壊します。 サスペンド中に状態が変化する可能性があり、別のスレッドで再開することもあります。すべてのawaitの後に前提を再確認してください。awaitをまたいでロックを保持しないでください。awaitをまたいでスレッドローカルストレージに依存しないでください。
アクターの再入可能性
アクターが保証するのは相互排他であり、トランザクションではありません。同じアクター上の 2 つの await の間には、他の処理が実行されます。
- アクターの状態は同期メソッドで変更してください。 アクター上の同期コードは中断されずに最後まで実行されます — これがトランザクション境界です。
- 非同期のアクターメソッドは薄く保ち、同期的なトランザクション操作で構成し、すべての
awaitの時点でアクターを一貫した状態に保ってください。 - 典型的なバグ: キャッシュを確認 →
awaitでダウンロード → キャッシュに書き込み。2 つのタスクが両方ともミスし、両方ともダウンロードし、2 番目が 1 番目を上書きします。awaitの後に再確認するか、実行中の処理を重複排除してください。 - アクターは FIFO ではありません。 優先度逆転を避けるために、優先度の高い処理を先に実行します。順序が必要な場合は、アクターではなくタスク(開始から終了まで実行される)または
AsyncStreamを使用してください。
4. Sendable とデータの共有
Sendable は、分離ドメイン間で共有しても安全な型を表します。コンパイラは、すべてのタスク境界とアクター境界でこれをチェックします。
- 値型は、そのストレージが
SendableであればSendableです — 非 public 型では自動的に推論されます。public 型の Sendable が推論されることは決してありません: public 型をSendableとマークすることはクライアントへの約束であるため、Swift はそれを明示的に書くことを求めます。 - アクターと
@MainActorクラスは暗黙的にSendableです。その状態が分離されているためです。 - ほとんどのモデルクラスは
@MainActorでもSendableでもあるべきではありません。 意図的に非Sendableのままにしてください — これにより、モデルの半分がメインスレッドで変更され、残りの半分がバックグラウンドで変更される事態を防げます。メインアクターから出る必要がある場合は、Sendableにするのではなくnonisolatedにしてください。 - 非
Sendableのオブジェクトでも、送信側がそれを使い終えていれば、ドメイン間で 送信 できます。 受け渡す 前に すべての変更を済ませてください。その後に触るとエラーになります。 - クロージャも状態をキャプチャします。関数型を
@Sendableとマークするのは、本当にドメインをまたぐ場合だけにしてください。 @unchecked Sendableはコンパイラがチェックできない約束です。 実際の内部同期(Mutex、ロック)を持つ型のために取っておいてください。グローバル変数に対するnonisolated(unsafe)も同様です — 最後の手段であり、警告を黙らせるためのものではありません。
データ競合エラーに遭遇したら、このリストを上から順にたどってください:
- 共有しない。 共有オブジェクトをローカルに移動し、各並行ジョブがそれぞれ独自のインスタンスを取得できるようにします。(これが実際のエラーの圧倒的多数に対する修正方法です。)
Sendableな値型にする — そうすれば「共有」は実際にはコピーになります。- アクターに分離する — メインアクター、または独自のアクターです。
- それをして初めて、
SynchronizationモジュールのMutex/Atomic(letプロパティに格納します)、または@unchecked Sendableに手を伸ばしてください。
グローバル変数と静的変数は、最も一般的なエラーの原因です。 優先順位の高い順に: let にする; @MainActor に置く; Mutex でラップする; nonisolated(unsafe)。Swift のグローバル変数は C とは異なり、遅延 かつ アトミックに初期化される点に注意してください。
古いコールバック API のブリッジ: 自分が所有するデリゲートプロトコルには @MainActor を注記してください。所有していない場合は、メソッドを nonisolated とマークし、MainActor.assumeIsolated { } を使用してください — これはホップするのではなくアサートするため、静かに競合するのではなく大きな音でトラップします。準拠に対する @preconcurrency は、同じことの省略形です。まだ移行していないモジュールからの Sendable 警告を一時的に黙らせるには @preconcurrency import を使用してください; そのモジュールが移行すれば、警告は — 正しく — 再び表示されます。
5. 構造化並行処理
常に構造化タスクを優先してください。
構造化タスク(async let、タスクグループ)はローカル変数のようにスコープを持ちます。ブロックを超えて生存することはできず、自動的に await され、タスクツリーを通じてキャンセル、優先度、タスクローカル値を継承します。非構造化タスク(Task { }、Task.detached)は、こうしたものを自動的に何も提供しません。
async letは、固定された静的に既知の数の並行子タスクに使用します。withTaskGroupは、数が動的な場合に使用します。タスクグループはAsyncSequenceに準拠しており、結果が到着するたびに反復処理できます。子タスクが何も返さない場合はwithDiscardingTaskGroupを使用します。各子タスクのリソースを即座に解放し、最初のエラーで兄弟タスクをキャンセルします。Task { }は、処理のライフタイムがスコープに収まらない場合にのみ使用します。デリゲートコールバックへの反応、ボタンのタップ、ビューの表示などです。これはアクター分離と優先度を継承しますが、キャンセルは自分で管理する必要があります。Task.detachedはほとんど使用しません。これは何も継承しません。分離も、優先度も、タスクローカル値もです。切り離されたルートが必要な場合は、繰り返し切り離すのではなく、その_内部_にタスクグループを配置してください。
キャンセルは協調的です。 キャンセルはフラグを設定するだけで、何も停止しません。高コストな処理を開始する前に Task.isCancelled を確認するか try Task.checkCancellation() を呼び出してください。同期ヘルパー内でも同様です。実行中ではなくサスペンドされている処理(AsyncSequence の next() など)には、withTaskCancellationHandler を使用します。そして、ハンドラーは本体と即座に、かつ並行して実行されることを忘れないでください。そのため、ハンドラーが触れる状態には真の同期(アトミックまたはロックであり、アクターではありません。アクターでは順序を保証できません)が必要です。
並行性を制限してください。 無制限のリストに対して、項目ごとに 1 つの子タスクを展開しないでください。N 個の子タスクを開始し、1 つが完了するたびに新しいものを追加します。
タスクローカル値(@TaskLocal)は、すべてのシグネチャにパラメータを通すことなく、コンテキスト(リクエスト ID、トレーススパンなど)をタスクツリーに伝播します。未バインドの読み取りに適切なデフォルト値が得られるよう、オプショナルにしてください。
コールバックのブリッジ: withCheckedContinuation / withCheckedThrowingContinuation。契約はすべてのパスで正確に 1 回 resume することです。resume しないと呼び出し元が永遠にハングし、2 回 resume すると致命的エラーになります。後から発火するデリゲート API の場合、continuation を保存し、resume するときに nil にします。(未リリースの Swift 6.4 では、単一 resume をコンパイル時にチェックする Continuation 型が追加されます。)
AsyncSequence: for await / for try await で反復処理します。既存のハンドラーまたはデリゲートベースの API は AsyncStream / AsyncThrowingStream で適合させます。クロージャ内でソースを構築し、ハンドラーから yield し、onTermination でクリーンアップします。
6. SwiftUI における並行処理
Viewは@MainActor分離されており、@Stateを含め、それが含むすべてのものも同様です。ビューやビューモデルに@MainActorを書く必要はほとんどありません。また、main-actor-by-default を使用すれば、既存のものを削除できます。- SwiftUI はフレームを安価に保つため、意図的に一部のコードをメインスレッド外で実行します。 そのシグナルは API のシグネチャにおける
@Sendableです。visualEffect、Shape.path(in:)、Layoutの要件、onGeometryChangeなどです。これらのクロージャ内で分離エラーに遭遇した場合、selfを送るのではなく、必要な 1 つの値をクロージャのキャプチャリストにコピーしてください。
.visualEffect { [pulse] effect, proxy in // copy the Bool, don't capture self
effect.blur(radius: pulse ? 2 : 0)
}
- SwiftUI のアクションコールバックは意図的に同期的です。 時間に敏感な UI 更新(ジェスチャーやスクロールイベントに応じてアニメーションを開始するなど)は、イベントと同じフレームで行う必要があります。
withAnimationによる状態変更は同期コールバック内に置き、それに続く長時間実行される処理にのみTaskを開いてください。 - UI と非同期処理の継ぎ目に状態を置いてください。 ビューがタスクを開始し、非同期レイヤーが完了時に同期的な変更を行い、UI が反応します。これにより、ビューロジックを同期的に保ち、SwiftUI をインポートせずに非同期ロジックをテスト可能にできます。
7. プロトコルとジェネリクス
クラスから始めないでください。プロトコルから始めるのもやめてください。
ワークフローは次のとおりです。具象型を書く → それらの間で繰り返されるコードに気づく → 共通の機能をプロトコルに抽出する → それに対してジェネリックなコードを書く。 ほぼ同一の本体を持つオーバーロードは、一般化すべきタイミングの合図です。
- 型ごとのカスタマイズがないプロトコルは無駄なプロトコルです。 すべての準拠が同じデフォルト実装を使うのであれば、既存のプロトコルに対する制約付き拡張を書いてください。凝ったプロトコル階層(「型の動物学」)はコンパイル時間とバイナリサイズを消費するだけで、何ももたらしません。
- is-a よりも has-a を優先してください。 プロトコルの操作の一部だけがあなたの型にとって意味を持つなら、それを改良するのではなく、ジェネリックな構造体でラップして、意図した API だけを公開してください。(
GeometricVector: SIMDではなくGeometricVector<Storage: SIMD>。) - プロトコルの要件はカスタマイズポイントです — 動的にディスパッチされ、準拠する型の実装がどこでも優先されます。拡張にのみ存在するメソッドは静的にディスパッチされるため、準拠する型のバージョンはそれをオーバーライドするのではなく_シャドウ_し、
any Pしか知らないコードは拡張のバージョンを呼び出します。ある型が何かをカスタマイズできるようにすべきなら、それを要件にしてください。 - 継承よりもコンポジション。 クラス継承はモノリシックで(スーパークラスは 1 つ)、押しつけがましく(格納プロパティとイニシャライザの複雑さを継承します)、何をオーバーライドしてよく、いつ super を呼ぶべきかについての取り決めが書かれていません。代わりに小さな値を合成してください。
- 強制ダウンキャストはコードの臭いです — 通常、型の関係がクラス階層や存在型によって失われたことを意味します。
some と any
- デフォルトでは
some Pを書いてください。任意の型を格納する必要があるときにany Pに変更します。varより先にletとするのと同じ規律です。 some P— スコープごとに 1 つの固定された基底型。関連型を含むすべての型の関係を保ち、コンパイラは特殊化できます。any P— 型消去されたボックスで、動的な型が実行時に変わります。異種コレクション、基底型のオプショナル性、抽象化を完全に隠すために必要です。その代償を払うことになります。関連型の関係は上限境界に消去され、呼び出しはオプティマイザにとって不透明です。- 関連型を取るメソッドを
any Pに対して呼び出すことはできません。 消去は生産位置では機能しますが(結果は上限境界に消去されます)、消費位置では機能しません。修正方法は、存在型をsome Pを取る関数に渡すことです — コンパイラがそれをアンボックスし、そのスコープ内では型が再び固定されます。 - 制約付き存在型と不透明型 —
some Collection<Element>、any Collection<any Animal>— は、要素型を公開しながらLazyFilterSequence<[Animal]>を隠すことを可能にします。独自のプロトコルで主関連型を宣言してください(protocol Container<Item>)。ただし、それは呼び出し側が実際に渡す型に対してであり、Iteratorのような実装の詳細に対してではありません。 where句の同一型要件は、プロトコルをまたいだ関係を確定する方法です(where Self.CropType.FeedType == Self)。それがなければ、「育ててから収穫する」は型チェックを通らず、誤った準拠がコンパイルを通ってしまいます。
8. API 設計 — 使用箇所における明確さ
使用箇所における明確さは、ここにある他のあらゆる目標よりも優先される目標です。
- Swift 専用 API では型プレフィックスを付けないでください。 モジュールが曖昧さを解消します。プレフィックスは、その API が Objective-C のものを踏襲している場合にのみ残してください。ただし、特定のフレームワーク由来の非常に一般的な名前は避けてください。文脈から外れると読みにくくなり、手動での曖昧さ解消を強いられます。
- 非同期の代替や、結果を直接返すものからは先頭の
getを外してください。getPersistentPostsではなくpersistentPostsです。 - アクセス制御はドキュメントです。
private(ファイル)、internal(モジュール、およびデフォルト)、package、public。境界で明示することが、上記の sendability と API 進化に関する判断を強制するのです。 - 不正な状態を記述できないようにモデルを設計してください。 private セッターと検証を行う mutating メソッド、閉じた集合には enum、
Stringの代わりに強く型付けされたUUIDを用います。 - プロパティラッパーは アクセスポリシー(
@Argument、@Published、防御的コピー、lazy、スレッドローカル)をくくり出し、宣言箇所でそのポリシーを一言で示せるようにします。キーパス上の@dynamicMemberLookupと組み合わせると、ラッパーを通して射影できます($binding.titleが機能するのはこの仕組みです)。 - 宣言的な DSL には結果ビルダーを。定型コードがコンパイラに書けたはずのコードである場合にはマクロを使用します(§12)。
9. パフォーマンス — 測定してから選択する
低レベルの Swift パフォーマンスは 4 つのコストに支配されます。どれを支払っているのかを把握してください。
- 関数呼び出し — 引数のコピー、静的ディスパッチと動的ディスパッチ、コールフレームの割り当て、そして最適化の妨げ。
- メモリレイアウト — インラインストレージとアウトオブラインストレージ、動的サイズ型。
- 割り当て — グローバル(無料)、スタック(安価: 減算 1 回)、ヒープ(高価: 検索とロック)。
- コピー — retain/release と再帰的な構造体のコピー。
ただし、まずアルゴリズムの作業を行ってください。 ループを書くたびに、それをアルゴリズムの呼び出しで置き換えられないか試してください。最大の成果は、ほとんどがマイクロ最適化ではありません。
- 呼び出すものの計算量を把握してください。
Array.remove(at:)は O(n) であり、ループ内で呼び出すと O(n²) になります。removeAll(where:)は全体で O(n) です。1 バイトごとに再スライスしてDataを構築すると O(n²) になりますが、popFirst()は O(1) です。どちらも、きれいに見えるコードの裏に隠れていた 100 倍以上のリグレッションでした。 - 連鎖した
map/flatMap/filterは各段階で配列を割り当てます。 エレガント ≠ 高速。パイプラインがホットループ内でピクセル単位や要素単位で実行される場合は、出力を一度だけサイズ設定してそこに書き込んでください。 - 次にプロファイリングを行ってください。 Instruments の Time Profiler と Allocations を、_テスト_に対して実行します(テストの実行ボタンを副ボタンクリック → Profile)。そうすれば、関心のあるコードを正確に測定できます。フレームグラフを支配する
platform_memmoveは意図しないコピー、100 万回の一時的な割り当ては中間配列、swift_beginAccessは実行時の排他性チェック、swift_retain/swift_releaseは参照カウントのトラフィックを意味します。
具体的な手段を、得られる効果が大きい順に概ね並べると:
- サブクラス化する意図のないクラスに付ける
finalは、動的ディスパッチを静的ディスパッチに変え、インライン化を可能にします。モジュール全体の最適化により、コンパイラが多くのケースでこれを証明できるようになります。さらにジェネリックの特殊化も可能になり、ジェネリクスがコストのかからないものになります。 - 構造体のストレージはインライン、クラスのストレージはアウトオブラインです。 小さな構造体は無料です。参照型のフィールドを3つ持つ大きな構造体は、_コピーごとに_3回のretainを要しますが、クラスなら1回です。頻繁にコピーするなら、コピーオンライトを使用してください。
any Pの存在型には3ワードのインラインバッファがあります。 収まる値はインラインに置かれ、それより大きなものはコピーごとにヒープ確保されます。同じテクニックが使えます。大きな型にコピーオンライト付きの間接ストレージを持たせれば、再びバッファに収まります。- 同種の
[MyModel]は[any Model]に勝ります — 密にパックされ、型情報は一度だけ渡され、特殊化可能です。[any Model]は柔軟だが不透明な選択肢です。必要ならこちらを選んでください。 - ジェネリックパラメータをクラスに制約する(
T: AnyObject)と、特殊化がなくてもコンパイラは既知の表現を得られます。 - 固定サイズのストレージには
InlineArray<N, T>(Swift 6.2)を使用します。要素はインラインに格納され、サイズは値ジェネリクスにより型に含まれ、ヒープ確保も参照カウントも、一意性チェックや排他性チェックもありません。コピーや共有が発生するなら誤った選択です。 Span/RawSpan/OutputSpan(Swift 6.2)は、連続したストレージへの直接アクセスのためにwithUnsafeBufferPointerを置き換えます。これらは非エスケープ可能なので、コンパイラがそのライフタイムをコンテナに結び付けます。ポインタのパフォーマンスを得ながらライフタイムのバグは発生せず、retain/release も消えます。- 格納プロパティをネストしたクラスから親の構造体へ移動すると、実行時の排他性チェックがなくなります。
- 必要性を測定したうえで、Swift 6.3 で出荷済み:
@inline(always)(メソッドにfinalを併用)と@specialized(where T == ...)(SE-0460)。ホットな具象型向けにジェネリックを事前特殊化します。 - Swift 6.4 で登場予定(未リリース — 下記§15の注記を参照):大きな格納値に対する
get/setの代わりとなるborrow/mutateアクセサ、UniqueArray/UniqueBox、そしてループから繰り返しのルックアップを巻き上げるRef/MutableRef。
非同期関数は、Cスタックではなくタスクごとのスラブアロケータ上に状態を保持し、各サスペンションポイントで部分関数に分割されます。コストプロファイルは同期関数に似ていますが、呼び出しのオーバーヘッドがわずかに高くなります。これは、待機するものがないものを async にすべきでないもう一つの理由です。
メインアクターとのホップには、実際のコンテキストスイッチのコストがかかります。 バッチ処理してください。イテレーションごとに2回ホップするのではなく、ループを loadArticles/updateUI の_中へ_押し込み、それらが配列を受け取るようにします。
10. ARC とオブジェクトのライフタイム
- オブジェクトの保証されたライフタイムは、閉じ波括弧ではなく最後の使用で終わります。 観測されるライフタイムはオプティマイザの創発的な特性であり、_必ず_変化します。
deinitがいつ実行されるかに依存するコードは潜在的なバグです。 weak/unownedは参照サイクルを断ち切るためのもので、それ以外ではありません。 強参照の所有者の最後の使用後にweak参照を読むと、正当にnilになることがあります。そこでオプショナルバインディングを使うのは強制アンラップより_悪い_です。大きなクラッシュを静かな誤った答えに変えてしまいます。weakより良い方法:サイクルを作らないこと。 共有データを第三の型に切り出し、双方がそれを参照することで、サイクルをツリーに変えます。- 次に良い方法:API を再設計することで、オブジェクトが強参照を通じてのみ到達可能になるようにします。
withExtendedLifetimeは機能しますが、正しさをあなたに押し付け、コードベース全体に広がります。設計ではなくパッチとして扱ってください。 deinitの副作用はローカルに保ってください。deinitからメトリクスを発行したりグローバルな効果を発火させたりすると、オプティマイザの決定に左右されます。代わりに呼び出し側でdeferを使い、deinitは検証用に残しておいてください。- Xcode の Optimize Object Lifetimes ビルド設定は、観測されるライフタイムを保証された最小値へと短縮し、まさにこれらのバグをあぶり出します。
11. テスト — デフォルトでは Swift Testing
新しいテストには Swift Testing を使用します。XCTest が今でも必要なのは、正確に次の 3 つだけです。UI 自動化(XCUIApplication)、パフォーマンス指標(XCTMetric)、および Objective-C で記述しなければならないテスト、または Objective-C の例外を捕捉するテストです。
- 任意の関数に
@Test— グローバル、静的、インスタンスのいずれでも。async、throws、グローバルアクター分離もすべて動作します。 #expect(...)は通常の式を受け取ります。 覚えなければならないXCTAssertEqualスタイルの関数群はありません —#expect(a == b)、#expect(list.isEmpty)、#expect(!x.contains(y))はすべて、失敗時に部分式の値を捕捉して表示します。try #require(...)は失敗時にテストを停止し、オプショナルを安全にアンラップします。これはcontinueAfterFailure = falseを置き換え、期待値ごとに選択できます。- スイートは
structです。 テスト関数ごとに新しいインスタンスが作成されるため、テスト間で状態が漏れることはありません。セットアップにはinitを使用します。ティアダウンにdeinitが必要な場合にのみclass/actorを使用します。スイートをネストしてグループ化します。 - コピー&ペーストやループの代わりにパラメータ化します。
@Test(arguments: [...])は各ケースを独立して、並列で、個別に再実行可能に実行し、失敗した引数を結果に表示します。2 つの引数コレクションは完全な直積を生成します — 一致するペアが必要な場合は代わりにzip()を使用します。 - トレイトは意図を伝えます。 条件には
.enabled(if:)/.disabled("reason")(テストをコメントアウトしないでください — 無効化されたテストもコンパイルされます)、追跡には.bug(url)、ファイルやターゲットをまたいでテストを関連付けるには.tags(...)、.timeLimit、テストが本当に並列実行できない場合は.serialized。ランタイムの#availableチェックではなく@availableを使用して、テストライブラリが認識できるようにします。 withKnownIssue { }は、自分の制御外の何かで失敗するテストに使用します —.disabledとは異なり、コンパイルと実行を続け、問題が修正されたときに知らせてくれます。confirmationは N 回発火するコールバックに。withCheckedContinuationは非同期オーバーロードのないワンショットコールバックに。- テストはデフォルトで並列に、ランダムな順序で実行されます。 これは機能です。隠れたテスト間の依存関係を明らかにします。
.serializedに頼るのではなくリファクタリングしましょう。 - 終了テスト —
#expect(processExitsWith: .failure) { ... }— は、分離された子プロセスでprecondition/fatalErrorのパスをカバーします。macOS、Linux、FreeBSD、Windows のみ。 - 移行: 両方のフレームワークは 1 つのターゲット内で共存するため、段階的に移行し、新しいテストは今日から Swift Testing で記述します。テストフレームワークの相互運用性(swift-testing ST-0021。利用可否はお使いの Xcode バージョンを確認してください)により、
XCTFailをラップするヘルパーを Swift Testing のテストから呼び出したり、その逆も可能になります。モードを complete または strict(limited ではなく、決して none でもなく)に設定して、フレームワーク間の問題がエラーのままであり、Issue.recordへの置き換えを指し示すようにします。
12. マクロ
コンパイラが導出できるコードを書いているときにマクロに手を伸ばしましょう — そしてそのときにだけ。
- マクロは展開前に型チェックされます。 引数はマクロの宣言されたシグネチャに対してチェックされるため、誤用は生成されたコード内の混乱ではなく、呼び出し箇所での明確なエラーになります。
- フリースタンディング(
#foo) は式または宣言を生成します。アタッチド(@Foo) は、メンバー、ピア、アクセサ、メンバー属性、準拠という 5 つの役割のいずれかで宣言を拡張します。役割は組み合わせ可能です —@Observableはメンバー + メンバー属性 + 準拠です。 - マクロは純粋な構文木変換としてテストします —
assertMacroExpansionを使用します。これが最速のループであり、誰も読まないコードのバグを避ける方法です。expansionにブレークポイントを設定し、構文ノードをpoしてその形を学びましょう。 - マクロが適用されないときは実際の診断を出力します。 エラーをスローするか、特定の場所で警告と修正候補には
context.addDiagnosticを使用します。マクロがコンパイルできないコードを黙って生成することを決して許さないでください。 - 展開されたコードは通常の Swift です。検査可能(「Expand Macro」)で、デバッグ可能で、ステップ実行可能です。
13. ログ記録とデバッグ
printではなくosのLoggerを使用します。 サブシステムとカテゴリごとに 1 つ作成します。メッセージは最適化された形式で保存され、表示されるときにのみレンダリングされるため、ログ記録は残しておけるほど低コストです。- 数値以外の補間はデフォルトで秘匿化されます。 本当に個人情報ではないデータに限り、値ごとに
privacy: .publicでオプトインします。値を公開せずに関連付けたい場合は.private(mask: .hash)を使用します。 - レベルは永続化とコストを制御します:
debug(永続化されず、最速 — ストリーミングしていないときはメッセージの構築が完全に最適化されて除去されます)、info、notice(デフォルト)、error、fault(最も永続化され、最も低速)。バグレポートに残しておきたいものにはerror/faultでログを記録します。 - 相関 ID(タスクまたはリクエストの UUID)をログに記録すれば、再現することなくデバイスのログアーカイブから障害全体の履歴を絞り込めます。
log collect --device --start ...を実行し、Console でサブシステムによってフィルタリングします。 format:とalign:は無料で使えます — ログを読みやすく、列選択可能にするために活用します。- LLDB は Swift のタスクを理解します。スレッドをまたいで
awaitをステップ実行し、swift task infoは優先度と子タスクを表示し、名前付きタスクはデバッガと Instruments の Swift Concurrency テンプレートの両方に表示されます。
14. 安全でないコードと相互運用
- 「安全でない(Unsafe)」とは、その API が入力を完全には検証できないため、その事前条件に違反すると未定義動作になるという意味です — クラッシュするという意味ではありません。安全な API は_意図的に_トラップします。クリーンな致命的エラーこそが安全な結果です。
Unsafe*PointerよりもSpanを優先します。 Swift 6.2 以降、連続したストレージにアクセスする安全で、非エスケープで、同等に高速な方法があります。生のポインタは C の相互運用のために取っておきます。- どうしてもポインタを使わなければならない場合: 安全でない領域をできるだけ小さく保ち、境界が追跡されるように、裸のポインタではなくバッファポインタ(アドレス + カウント)を使用し、ポインタがそれを提供するクロージャの外にエスケープすることを決して許さず、Address Sanitizer を実行します。
- セキュリティ上重要なモジュールでは厳格なメモリ安全性を有効にします — これにより、すべての安全でない使用をソース内で承認することが強制され、監査が可能になります。Swift 6.4 の
@diagnose属性(未リリース)を使うと、個々の関数に対して有効にできます。 - 相互運用は双方向かつ段階的です。 C、Objective-C、C++ の型は Swift に直接マッピングされます(C++ の値セマンティクス、Swift コレクションとしてのコンテナ、
~Copyableとしての移動専用型を含みます)。Swift 6.3 の@c属性は Swift の関数を C に公開します(宣言がすでにヘッダーに存在する場合は@implementationを使用します)。Swift は一度に 1 ファイルずつ採用してください。書き直さないでください。
15. 使うべきモダンな構文
エージェントはこれらすべてについて、日常的に古くて冗長な形式を書きます。
⚠ が付いた行は Swift 6.4 であり、まだ出荷されていません。 現在のリリースは 6.3.x です。これらのプロポーザルは承認され、main に実装されているため、計画するのは安全で、今日書くのは安全ではありません — 使用する前にプロジェクトのツールチェーンを確認し、6.3 以前を対象とする場合は古い形式を優先してください。
| 使わない方がよいもの | 代わりに書くもの | 導入バージョン |
|---|---|---|
ネストした三項演算子、let を初期化するための即時実行クロージャ | if/switch 式 | 5.9 |
| 引数1個、2個、3個…ごとのオーバーロード | パラメータパック(each T)、およびパックに対する for | 5.9 |
すべてのプロパティに ObservableObject + @Published | @Observable | 5.9 |
| オブジェクトの変更をポーリングする | Observations { ... } — トランザクション更新の AsyncSequence | 6.2 |
文字列型の userInfo を使う NotificationCenter | 具象通知型(MainActorMessage / AsyncMessage) | 6.2 |
スクリプト処理のための Process + パイプ | Subprocess パッケージ(1行ずつの出力には AsyncBufferSequence.strings()、1.0 は 6.4 で登場) | 6.2+ |
| 手書きの文字列インデックス計算 | Swift Regex — 簡潔さにはリテラル、構造化には RegexBuilder | 5.7 |
ホットパス内の固定サイズの [String] | InlineArray<N, T> | 6.2 |
withUnsafeBufferPointer | .span / .bytes(RawSpan)/ OutputSpan | 6.2 |
書き込みを完了するための手動での Task.isCancelled の扱い | Task のキャンセルシールド(SE-0504) | 6.4 ⚠ |
| キーを使うために辞書を手作業で再構築する | mapKeyedValues | 6.4 ⚠ |
@available(iOS ..., macOS ..., tvOS ..., watchOS ..., visionOS ...) | @available(anyAppleOS ...) | 6.4 ⚠ |
型がモジュールを隠蔽してしまう場合の Rocket.SaturnV | モジュールセレクタ Rocket::SaturnV | 6.3 |
| 一律の「警告をエラーとして扱う」 | 宣言単位/警告グループ単位の @diagnose | 6.4 ⚠ |
weak var が原因での @unchecked Sendable | weak let、または ~Sendable で非 Sendable 性を明示する | 6.4 ⚠ |
| ポインタを使ってバイナリ形式を手動で解析する | Swift Binary Parsing(ParserSpan、オーバーフローチェック付きの解析イニシャライザ) | 6.2 |
| ぎこちないテスト関数名 | 生識別子: @Test func `fruits have a tropical climate`() | 6.0 |
また知っておく価値があること: Swift Regex のパーサーは Foundation の実際のパーサー(.date(...)、.currency(...))と組み合わせて使えます — 正規表現内で日付や数値の解析を手書きで実装しないでください。システムのロケールを継承するのではなく、ロケールを明示してください。そして、パターンが入力全体をまたいでバックトラックするのを防ぐには、NegativeLookahead や Local(アトミックグループ)を使用してください。
16. 既存のコードベースを Swift 6 に移行する
順序が重要であり、手順を混在させると移行が行き詰まります。
- まず新しいコンパイラでビルドします。 ソース互換性があるため、Swift 5 モードでそのまま動作するはずです。
- ターゲットごとに完全な並行性チェックを有効にします(Swift 5 モード + すべての Swift 6 警告)。その下にあるフレームワークではなく、UI/アプリ層から始めてください。その多くはすでに SDK によってメインアクター注釈が付けられているため、修正率が高くなります。
- 警告を修正します。コストの低いものから順に。 少数の根本原因から数百の警告が発生することが予想されます。
letにすべきvarグローバル、@MainActorに属すべきフリー関数、: Sendableが必要な 1 つのパブリック構造体などです。1 行で数十個が解消されることもあります。 - ターゲットを Swift 6 言語モードに切り替えて、作業を確定します。
- 次のターゲットに移り、繰り返します。
- その後、リファクタリングは別途行います。 大規模なリファクタリングとデータ競合安全性の有効化を決して組み合わせないでください。両方とも元に戻さなければならなくなります。
厳格なチェックを再びオフにして出荷することもできます。あなたが行ったすべての修正は、そのまま残る genuine な改善です。開始する前に Approachable Concurrency を、そしてアプリモジュールではデフォルトでメインアクターを有効にしてください。どちらも表示されるエラーの数を劇的に減らし、Xcode には多くの変更を自動で適用する移行ツールが同梱されています(swift.org/migration)。
クイックリファレンス
| 必要なもの | 使うべきもの | 使うべきでないもの |
|---|---|---|
| データ型 | struct / enum | アイデンティティや共有を伴わない class |
| 共有された可変状態 | actor、または @MainActor クラス | class + 忘れずに管理しなければならないロック |
| メインスレッドから処理を移す | @concurrent func … async | Task.detached、DispatchQueue.global() |
| ライブラリ API の分離 | nonisolated | @MainActor、@concurrent |
| 並列ジョブ数を固定 | async let | 構造化されていない N 個の Task |
| 並列ジョブ数を動的に | withTaskGroup(上限あり) | 要素ごとに 1 タスク、上限なし |
| 何も返さない子タスク | withDiscardingTaskGroup | 決して drain しない withTaskGroup |
| UI イベントに紐づく処理 | コールバック内の Task { } | コールバックを async にすること |
| データ競合の修正 | オブジェクトの共有をやめる | @unchecked Sendable |
| 共有モデルクラス | 非 Sendable、または @MainActor | Sendable + 手動ロック |
await をまたぐブロッキングプリミティブ | なし — 構造を再設計 | DispatchSemaphore、NSCondition |
| ポリモーフィズム | some P | ストレージが必要でない限り any P |
| 異種コレクション | [any P] | クラス階層 |
| カスタマイズ不要な共有動作 | 制約付き extension | 新しいプロトコル |
| カスタマイズポイント | プロトコルの要件 | 拡張にのみ存在するメソッド |
| 参照サイクルの解消 | ツリー構造に再設計 | weak + withExtendedLifetime |
| 一致する要素の削除 | removeAll(where:) — O(n) | ループ内の remove(at:) — O(n²) |
| 連続メモリへの直接アクセス | .span | withUnsafeBufferPointer |
| ホットパスでの固定サイズバッファ | InlineArray<N, T> | Array |
| 新しいテスト | @Test + #expect | XCTestCase + XCTAssertEqual |
| 多数の入力に対する同じテスト | @Test(arguments:) | for ループ、またはコピー&ペースト |
| 失敗時にテストを停止 | try #require | continueAfterFailure = false |
| 一時的に壊れているテスト | withKnownIssue | .disabled、またはコメントアウト |
| 出荷コードでの診断 | Logger + 相関 ID | print |
| 最適化の判断 | プロファイリング済みテストでの Instruments | 直感 |