並行処理の基本から実践的なガイダンスまでてんこもり。@MainActor 隔離や Sendable の扱いについて、いまだにコンパイルエラーに振り回されている状況で、理解し使いこなせている感じがしないのだが、何のためにそうしたアノテーションが存在するのか、理解に近づいた気がした。
0:00 – Introduction
- SwiftUI アプリ開発における並行処理について
- データレースバグ(予期しないアプリ状態、ちらつくアニメーション、永続的なデータ損失)からの安全性を確保
- SwiftUI がコードを様々な方法で並行実行する仕組みと、API の並行処理アノテーションによる識別方法を学習
- Swift 6.2 の新しい言語モードでは、モジュール内のすべての型に
@MainActorアノテーションが暗黙的に付与
2:13 – Main-actor Meadows
- @MainActor の基本概念:
- SwiftUI の View プロトコルは
@MainActor隔離を宣言 - e.g. ColorExtractorView が View に準拠するため、
@MainActor隔離される- コンパイル時に暗黙的に適用されるが実際のコードには含まれない
- SwiftUI の View プロトコルは
- 型レベルでの隔離効果:
- 型全体が
@MainActorで隔離されると、そのすべてのメンバーも暗黙的に隔離されるbodyプロパティ、@State変数なども含む
- 共有された
@MainActor隔離により、これらのアクセスはコンパイラによって安全であることが保証
- 型全体が
- コンパイル時のデフォルト:
@MainActorは SwiftUI のコンパイル時デフォルト- ほとんどの場合、アプリ機能の構築に集中でき、 並行処理について考える必要がない、並行処理目的のためのコードアノテーションは不要で、自動的に安全
- データモデルとの連携:
- データモデル型には
@MainActorアノテーションが不要 - ビュー宣言内でモデルをインスタンス化すると、Swift がモデルインスタンスの適切な隔離を保証
- データモデル型には
- 非同期コンテキストでの利用:
bodyが@MainActor隔離されているため、例では.onTapGestureで実行しているTask { }クロージャもメインスレッドで実行
- AppKit/UIKit との相互運用性:
- AppKit と UIKit の API は排他的に
@MainActor隔離- SwiftUI はこれらのフレームワークとシームレスに相互運用
UIViewRepresentableプロトコルはViewプロトコルを継承し、@MainActor隔離UILabelのイニシャライザも@MainActor隔離を要求 →@MainActor隔離のmakeUIViewで機能
- AppKit と UIKit の API は排他的に
- ランタイムセマンティクスの表現:
- SwiftUI の並行処理アノテーションはランタイムセマンティクスを表現
- アノテーションはフレームワークの意図されたランタイムセマンティクスの下流
7:17 – Concurrency Cliffs
- パフォーマンス最適化の必要性:
- アプリ機能の追加により、メインスレッドに負荷がかかりフレーム落ちやちらつきが発生
- Task と構造化 Concurrency を使用してメインスレッドから計算処理を切り離し
- SwiftUI の組み込みアニメーション最適化:
- 組み込みアニメーションはバックグラウンドスレッドで中間状態を計算
scaleEffectアニメーションの例:1から1.5の間で異なるスケール値をフレームごとに計算- 複雑な数学計算を含むアニメーション値の計算は高コストなため、バックグラウンドスレッドで実行
- 組み込みアニメーションはバックグラウンドスレッドで中間状態を計算
- 宣言的特性の活用:
- SwiftUI は宣言的で、View プロトコルに準拠する構造体はメモリ内の固定位置を占有しない
- ランタイムで SwiftUI がビュー個別の表現を作成
- この表現により多くの最適化の機会を提供、重要なのはバックグラウンドスレッドでのビュー表現の一部評価
- バックグラウンド実行の対象 API:
Shapeプロトコル:pathメソッドがバックグラウンドスレッドから呼び出される可能性visualEffect:視覚効果が高コストなため、クロージャがバックグラウンドスレッドから呼び出される可能性Layoutプロトコル:要求メソッドがメインスレッド外で呼び出される可能性onGeometryChange:最初の引数のクロージャがバックグラウンドスレッドから呼び出される可能性
- Sendable アノテーション:
- SwiftUI は
Sendableアノテーションでコンパイラと開発者にランタイム動作を表現 Sendableは@MainActorからデータを共有する際の潜在的なデータレースが生じる可能性があることを思い出させるもの(危険立ち入り禁止の標識のようなもの)- Swift はコードの潜在的なレース条件を確実に発見し、コンパイラエラーで通知
- SwiftUI は
- データレース回避戦略:
- データ競合を回避する最良の戦略は、並行タスク間でデータをまったく共有しないこと
- SwiftUI API が sendable 関数を要求する場合、フレームワークが必要な変数の大部分を関数引数として提供
- e.g.
sizeThatFits(…)が提供するsubviewsパラメタ:外部変数を利用せず高度な計算が可能
- e.g.
- 外部変数へのアクセス:
- sendable 関数で外部変数にアクセスする場合の制約
@MainActor隔離変数を sendable クロージャで共有する一般的なシナリオselfが Main actor から Background thread のコード領域に境界を越える必要がある- 「変数 self を バックグラウンドスレッドに送信する(Send self from main actor)」
- この「送信」には self の型が Sendable である必要(非隔離領域での参照)
- self のプロパティにバックグラウンドからアクセスするには、そのプロパティがどのアクターにも隔離されないことが必要(properties must be nonisolated)→ 解決方法へ
- 問題解決方法:
- View への参照を通じたプロパティ読み取りを避ける:クロージャのキャプチャリストで変数のコピーを作成し送信 e.g.
{ [pulse] in ...- コピーを参照することで
selfをクロージャに送信することを回避 - Bool などの単純な値型は sendable のため、コピーが可能
- 関数スコープ内でのみ存在するコピーへのアクセスはデータレース問題を引き起こさない
- コピーを参照することで
- 参照すべてのプロパティを非隔離状態(nonisolated)にする
- View への参照を通じたプロパティ読み取りを避ける:クロージャのキャプチャリストで変数のコピーを作成し送信 e.g.
16:53 – Code Camp
- 同期 API の設計理念:
- ほとんどの SwiftUI API(
Buttonのアクションコールバックなど)は同期的 - 並行コードを呼び出すには、まず Task で非同期コンテキストに切り替える必要
Buttonが非同期クロージャを受け取らない理由:同期的な更新が良好なユーザー体験において重要(特に長時間のタスクの前には)
- ほとんどの SwiftUI API(
- 長時間実行タスクでの UI 更新:
- 長時間実行タスクがあり、結果を待つ必要がある場合に特に重要
- 非同期関数で長時間実行タスクを開始する前に、進行中であることを示すための UI 更新が重要
- この更新は同期的であるべき、特に時間に敏感なアニメーションをトリガーする場合
- 実例:言語モデルによる色抽出:
withAnimationを使用して同期的に様々なローディング状態をトリガー- タスク完了時に別の同期状態変更でローディング状態を反転
- フレームレートの制約:
- UI フレームワークとして、SwiftUI は滑らかなインタラクションのためにデバイスの画面リフレッシュレートの現実に対処する必要
- スクロールなどの連続ジェスチャーに反応する際の重要なコンテキスト
- 非同期処理とアニメーションのタイミング:
onScrollVisibilityChangeでスクロール可視性の変化を検出- 状態変数を
trueに設定してアニメーションをトリガー - 非同期作業前にアニメーション変更を追加する場合のタイミング問題
- 一時停止ポイント(suspension point) の影響:
awaitで非同期関数を呼ぶと一時停止ポイントが作成されるTaskは非同期関数を引数として受け取る- コンパイラが
awaitを見ると、非同期関数を2つの部分に分割- 最初の部分を実行後、Swift ランタイムは関数を一時停止し、CPU で他の作業を実行可能
- この中断により、タスククロージャがデバイスのリフレッシュ期限を過ぎるまで再開されない可能性
- → 非同期関数では不足、同期コールバックが必要
- 同期コールバックの利点:
- SwiftUI はデフォルトで同期コールバックを提供
- 非同期コードの意図しない中断を回避
- 同期アクションクロージャ内での UI 更新は正しく実行しやすい
Taskを使用して非同期コンテキストにオプトインする選択肢は常に存在- 同期コードが多くのアプリにとっての優れた出発点・終着点
- UI と非 UI コードの境界分離:
- アプリが多くの並行作業を行う場合、UI コードと非 UI コードの境界を見つける
- 非同期処理のロジックをビューロジックから分離することがベスト
- 状態をブリッジとして使用:状態が UI コードと非同期コードを分離
- UI ロジックはほとんど同期的に保つ
- 非同期コードのテストが UI ロジックから独立するため、より簡単になる
- 構造的な改善:
- 時間に敏感な変更を多く必要とする UI コードと長時間実行非同期ロジックの境界を見つけることが重要
- ビューを同期的かつレスポンシブに保つのに役立つ
- 非 UI コードも適切に整理することが重要
23:47 – Next steps
- Swift 6.2 の活用:
- 優れたデフォルトアクター分離設定を提供
- 既存アプリでの試用を推奨、ほとんどの
@MainActorアノテーションを削除可能
- Mutex の活用:
- クラスを sendable にするための重要なツール
- 公式ドキュメントで学習方法を確認
- 単体テストの挑戦:
- アプリの非同期コードに対する単体テストの作成
- SwiftUI をインポートせずに実行できるかチャレンジ
- まとめ:
- SwiftUI が Swift Concurrency を活用して高速でデータレースフリーなアプリ構築を支援
- SwiftUI における並行処理の確固としたメンタルモデルの獲得を目標