ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
Swift 6へのアプリの移行
既存のサンプルアプリを更新するデモで、Swift 6への移行の実際の流れを確認しましょう。モジュールごとに段階的に移行する方法や、データ競合のリスクのあるコードの特定に役立つコンパイラの機能について解説します。 隔離の境界を明確に定義し、共有されたミュータブルステートのコードへの並列アクセスを排除するための、各種の手法もご紹介します。
関連する章
- 0:00 - Introduction
- 0:33 - The Coffee Tracker app
- 0:45 - Review the refactor from WWDC21
- 3:20 - Swift 6 and data-race safety
- 4:40 - Swift 6 migration in practice
- 7:26 - The strategy
- 8:53 - Adopting concurrency features
- 11:05 - Enabling complete checking in the watch extension
- 13:05 - Shared mutable state in global variables
- 17:04 - Shared mutable state in global instances and functions
- 19:29 - Delegate callbacks and concurrency
- 23:40 - Guaranteeing data-race safety with code you don’t maintain
- 25:51 - Enabling the Swift 6 language mode in the watch extension
- 26:35 - Moving on to CoffeeKit
- 27:24 - Enabling complete checking in CoffeeKit
- 27:47 - Common patterns and an incremental strategy
- 29:55 - Global variables in CoffeeKit
- 31:05 - Sending an array between actors
- 33:53 - What if you can’t mark something as Sendable?
- 35:23 - Enabling the Swift 6 language mode in CoffeeKit
- 35:59 - Adding a new feature with guaranteed data-race safety
- 40:43 - Wrap up and the Swift 6 migration guide
リソース
関連ビデオ
WWDC21
-
ダウンロード
こんにちは SwiftチームのBenです 本セッションでは 既存のアプリで Swift 6の言語モードを 有効にする手順を説明します 競合に対する安全性を Swift 6がどのように実現するかを確認し この変更を アプリに段階的に導入するための手法や Swiftの並列処理に関する保証を まだ認識していない フレームワークとのやり取りに 対処する方法も確認します 今回使用するのは 一日に飲むコーヒーの量を トラッキングする シンプルなアプリと 現在のカフェイン摂取量を 文字盤に表示するコンプリケーションです WWDC21で Swiftの並列処理を初めて発表した時 Swiftの新しい並列処理モデルを このアプリで採用する方法を説明しました その解説では 一見すっきりとしたアプリアーキテクチャが 実際には 複雑な並列処理が 行われている場合があることを確認しました ビューやモデルを見ると すべてがきれいに整理されていますが 並列処理がどう行われているか調べると 別の様相が見えてきます 元のアプリには コードが随時実行される 3つの並列キューがありました UIとモデルでの操作は メインキューで実行されていました また バックグラウンドの処理を行う ディスパッチキューもありました さらに HealthKitから結果を返す コールバックのような 完了ハンドラへの 特定のコールバックを実行する 任意キューもありました このように 型はきれいに整理されていましたが アプリ全体での並列処理のあり方は そこまで明確ではありませんでした
どのキューがコードを実行しているのか それはなぜなのかといった処理の全体を いくつもの型によって様々な場所で 不明瞭な形で構成していました
Swiftの並列処理を採用することで この場当たり的な 並列処理アーキテクチャから ご覧のような形へと移行しました UIビューとモデルは メインアクターで実行されるように設定され バックグラウンドの処理は 専用アクターで実行されるようになりました アクターは Swiftの async/await機能を使用して スレッドセーフな値型で互いに通信します これにより 並列処理のアーキテクチャは 型アーキテクチャと同じくらい 明確で説明しやすくなりました ただ 問題が1つありました アーキテクチャを改善するために このリファクタリングを行う際 プログラマーである私には データ競合を回避するという 大きな責任がありました すべてのガイドラインに従い アクター間の通信には値型を使いましたが 必ずしもそうする必要はありませんでした 例えば クラスなどの参照型を取り あるアクターから別のアクターに 渡すこともできました 参照型を使用すると 共有のミュータブルステートを渡せます ただし そうすると 両方のアクターが共有されたステートに 同時にアクセスできるようになるため アクターによる相互排他が 機能しなくなります つまり クラスのインスタンスを あるアクターから別のアクターに送信すると データ競合が発生して プログラムのクラッシュ さらには ユーザーデータの破損さえ生じかねません そこで重要になるのが Swift 6の利点です
Swift 6の言語モードでは データ分離を全面的に適用できます コンパイラにより 先述のようなタスク間やアクター間での 不適切なステートの共有が 防止されるので リファクタリングの実行や アプリへの新機能の追加の際に 並列処理のバグを 新たに生じさせる心配がなくなります
Swift 6の言語モードは 新規と既存の両方のプロジェクトで 利用できます このモードを導入すると アプリの品質が大幅に向上します 並列コードにおけるミスを コンパイル時に検出できるからです この機能が特に役立つのは 再現が難しいクラッシュが 発生している場合に データ競合のリスクを 詳しく把握して 体系的に排除したいような時です また 応答性とパフォーマンスを 向上させるために 並列処理の統合に 積極的に取り組んでいる場合は Swift 6モードを導入すれば 変更により 新たなデータ競合が生じるリスクを 確実に排除できます
Swiftの公開パッケージを 管理している場合は コードベースを移行したいと考えている ユーザーを支援するために できるだけ早くSwift 6を 導入することをお勧めします 同様にSwift 6を導入済みの 依存関係に基づいて開発できるため ユーザーにとってもメリットになります 人気のあるパッケージでの Swift 6の導入状況は swiftpackageindex.comで 誰でも確認できます 本セッションでは Swift 6が 実際にどう機能するかをご紹介します CoffeeTrackerアプリで Swiftのデータ分離機能を有効にします 手順を追いながら コンパイラから提供される ガイダンスに沿って CoffeeTrackerにおいて データ競合が発生しないことを Swiftで保証できるよう 変更を加えるべき箇所を見ていきます 今のところ 私のアプリにはデータ競合が 含まれていないと確信しています そしてきっと みなさんのコードも 同じ状況だと思います 長年の改良やバグレポートを通じて また メインスレッドチェッカーや スレッドサニタイザーなどを使用して 既存のコードに含まれるデータ競合の大半を すでに解消している場合もあるでしょう データ競合安全の真の価値は 新しくコードを記述する際に バグの混入を防げることです 新しい機能を追加する際や 並列処理を有効に活用するために コードをリファクタリングする際もです データ競合安全により アプリで並列処理を活用できます 新たなデータ競合が生じるリスクを 恐れることなく 後で追跡したり推測に基づく修正を 加える必要がありません クラッシュを再現できないからです
前回ご紹介した時から CoffeeTrackerアプリはとても好評なため チームを拡大して 新機能の追加を開始しました
その一環として アプリの一部を 新フレームワークのCoffeeKitに組み込み コードの一部が現在そこにあります 新機能をアプリに追加することに チームは意欲的ですが その前に アプリを Swift 6に更新するつもりです そうすれば 新機能の追加を進めても 並列処理のバグが生じることはありません 今 Xcode 16をダウンロードして 開いたところです つまり 新しいSwift 6コンパイラと watchOS 11用の最新のSDKがあります アプリをビルドしてみましょう
うまくビルドできました 更新の必要はありません ただし それは データ競合の恐れがないからではありません 単にSwift 6の言語モードを まだ有効にしていないだけです 以前のリリースと同様に Swift 6では ソース互換性が保証されます ごくわずかな変更を除き アプリは常に新しいコンパイラで ビルドできるはずです
アプリを最新のXcodeで ビルドできることが分かったので 次のステップに進み Swift 6モードを有効にして データ分離を全面的に適用しましょう そのための準備として コンパイラ診断を有効にする前に MainActorとSendableの監査を 行ってみることもできます ただしその場合は 新しいSwiftコンパイラが もたらすメリットを活用できません コンパイラ診断により 修正が必要な箇所がわかります パートナーがコードのバグを指摘してくれる ペアプログラミングのようなものです これにより一定の構造を 移行プロセスに追加できます 段階的な手順に沿って コード内の各ターゲットを移行していきます 各ターゲットについて 次の手順を実行します まず 並列処理の完全なチェックを 有効にします これはモジュールごとの設定で プロジェクトはSwift 5モードのままですが Swift 6の強制的なデータ分離では エラーとして検出されるすべてのコードに 警告が表示されるようになります これらのターゲットに表示された警告を すべて解決していきます 完了したら Swift 6モードを有効にします これにより変更がすべてロックされ 将来のリファクタリングで 安全でない状態に戻ることが防止されます その後 次のターゲットに移り 同じプロセスを繰り返します この機能を有効にしたあと 改めて アプリ全体を リファクタリングすることもできます それにより アーキテクチャを変更して 安全でないオプトアウトを取り消したり コードを改良するためのリファクタリングを 実行したりできます リファクタリングに関するアドバイスを 1つ紹介します 大幅なリファクタリングと データ競合安全の有効化を 一緒に行うことは できるだけ避けてください 1つずつ進めてください 両方を同時に実行しようとすると 多くの場合は 一度に変更するものが多くなりすぎて 後戻りしなければなりません
ここでは Swiftの並列処理を使うよう すでにリファクタリングしたアプリで Swift 6を有効にする手順のみに 焦点を当てます では まず 完全なチェックを有効にします 完全なチェックで何が可能になるでしょう Swiftの並列処理を アプリですでに使用している方は Swiftの並列処理機能の導入時に発生した 並列処理の問題に関する Swiftコンパイラの警告やエラーを 見たことがあるでしょう 例えば ここでは 新しいデリゲートを追加しています カフェインレベルが心配なほど少ない時に CoffeeKitから通知してもらうために 追加しているコールバックを 受け取ることが目的です このデリゲートは SwiftUIビューに値を公開するので メインアクターに分離する必要があります @MainActorを先頭に追加すると
そのすべてのメソッドと プロパティのアクセスを メインスレッドで行うように指示できます ただし このようにすると プロトコルの実装に関するコードの下部で エラーが発生します
次のような内容です 「Main actorで分離された インスタンスメソッド'caffeineLevel(at:)'は 非分離のプロトコル要件を満たすために 使用することができません」 このCaffeineThresholdDelegate プロトコルについては
どのように呼び出されるか 現時点では何の保証もしていません CoffeeKitに含まれていますが これはまだ Swift 6に更新されていません ただし ここでは Recaffeinater型をそれに準拠させて メインアクターに制約しています そのメソッドは メインアクターで実行されるため メインアクターで呼び出されることが 必ずしも保証されないプロトコルに 準拠させることはできません この問題は 後ほど改めて解決しますが これは Swiftコンパイラで生成される エラーの一例で メインアクターで型が呼び出されているかを チェックするよう選択したために発生します 以前のセッションでも説明したように Coffee Trackerに Swiftの並列処理を導入すると こうした問題が 色々と発生することになります アプリ内の様々な箇所に 並列処理が導入されるためです
ターゲットのビルド設定で 厳密なチェックを有効にすると 競合状態が生じる可能性があるか モジュール全体でチェックされます では これを有効にして どうなるか見てみましょう Swiftのデータ分離は ターゲットごとに有効にします このアプリには 主なターゲットが2つあります
1つはUIレイヤーが存在する WatchKit Extensionで もう1つはカフェインをトラッキングし HealthKitに保存する ビジネスロジックが存在する フレームワークのCoffeeKitです
まず WatchKit Extensionで 完全なチェックを有効にしましょう これには2つの理由があります 1つ目は 通常こちらの方がより簡単に 並列処理チェックを有効化できることです UIレイヤーの大部分は メインスレッドで実行され SwiftUIやUIKitなどの APIを使いますが これらのAPI自体はメインスレッドで 操作を実行することが保証されています 理由の2つ目は 厳密な並列処理を有効にする際 多くの場合 Swiftの並列処理に 対応するための更新が済んでいない ほかのモジュールを 操作する必要があることです 更新されることのない Cライブラリを使っているかもしれません または Swift 6にこれから更新する予定の フレームワークや パッケージモジュールかもしれません そしてもちろん 今回の CoffeeKitフレームワークも含まれます こうしたトップダウン方式がなぜ良いのか 実際に始めてみるとわかります では 最初のステップとして Extensionを選択し その設定に移動しましょう そして Swiftの並列処理チェック設定を検索し
完全なチェックとなるように に設定します
このように設定すると 並列処理の 安全性が確認できないコードについて コンパイラが警告を出すようになります これらは単なる警告で プロジェクトのビルドと実行は可能です ビルドしてみましょう
すると 先ほど見た警告のほかに いくつかの警告が表示されます 確認してみましょう
さて 最初の問題は Swift 6で対処しなければならない よくある問題の1つで この「logger」変数に関するものです
このように loggerインスタンスは グローバル変数として宣言されています グローバル変数は 共有された ミュータブルステートのソースであり プログラム内のすべてのコードが 実行スレッドに関係なく この同じ変数を読み書きできます つまりこれは データ競合の原因になりやすいので 安全にする必要があります その方法は複数あります 実際に問題を開いてみると
推奨の方法がいくつか示されています
最初の方法が最も簡単で 読み取り専用にするだけです loggerはSendable型です つまり let変数として宣言されている場合は 複数のスレッドから使用しても データ競合は発生しません varからletに置き換えてみましょう 再ビルドします
この問題は解決です これは適切な修正ですが ほかにも方法があります
イミュータブルにしたくないとしましょう 後で値を更新したいなどの理由で letではなくvarのままにしたい場合です
この場合使えるのは このグローバル変数を グローバルアクターに紐づける方法です これはUIレイヤー内であるため すべてのログ記録は メインアクターで行われるでしょう そこで このグローバル変数に @MainActorという注釈をつけます
これで すべてのログ記録の使用が メインアクターからとなり やはり 警告が消えます
最後の例として コンパイラでは強制できない ほかの外部メカニズムで この変数を保護する場合もあります 例えば ディスパッチキューで すべてのアクセスを保護しているとします この場合 nonisolated(unsafe)キーワードを使えます
Swiftのほかの箇所で 「unsafe」という語を使う場合と同様に この変数の安全性を確保することは ユーザーの責任になります これは最後の手段であるため 通常は Swiftによるコンパイル時の保証を 使うことをおすすめします しかし コンパイラが すべてを把握することはできないので そのような場合には 1つの方法として nonisolated(unsafe)を使用できます この方法は 後からもう一度 コードのリファクタリングが必要になる ケースの例かもしれません この変数をアクターに移動し 変数が安全に使われていることを コンパイラで検証可能にするためにです ただ この時点では これをnonisolated(unsafe)としてマークし 次の警告に進むこともできます 今回はそのようなケースではないので 元に戻して この変数をletとして宣言します
これが最善の方法です さて グローバル変数を初期化する このイニシャライザについて
次のような 疑問が生じるかもしれません いつ実行されるのか という点や それを知ることはスレッドの安全性の 理解のために重要ではないか という点です
Swiftのグローバル変数は 遅延初期化されます 値が初期化されるのは最初の使用時 つまり CoffeeTrackerが 初めてログを記録する時です これはCやObjective-Cと比べた場合の 非常に重要な違いです これらの言語では グローバル変数は起動時に初期化されます これは起動時間に 大きな悪影響を及ぼします Swiftの遅延初期化では こうした問題を避けて より早くアプリが使える状態になります しかし 遅延初期化は 競合の原因ともなる可能性があります 2つのスレッドが 同時にこのグローバル変数を使用して 初めてログを記録しようとしたら どうなるでしょうか loggerを2つ作成できるでしょうか Swiftでは そのような心配は不要です グローバル変数が アトミックに 作成されることが保証されているためです 2つのスレッドがloggerに 同時に かつ初めて アクセスしようとした場合 一方だけがこの変数を初期化し もう一方はブロックされて待機します これで問題は解決したので 次の問題を見てみましょう
こちらは WKApplicationの 共有インスタンスにアクセスするコードです このグローバルインスタンスメソッドは メインアクターに分離された要素の一例です
ここで最初に注意すべき点は アクターに分離されたステートの呼び出しは 暗黙的に非同期であることです つまり これが非同期関数の場合 awaitを使用してメインアクターの このグローバル変数にアクセスできます この関数は非同期ではないので 非同期としてマークするか 新しいタスクの開始が必要です ただし コンパイラは 別の修正法を提案しています
この関数を メインアクターに配置するだけです
これはビューのメソッドではなく フリー関数なので デフォルトでは メインアクターに配置されません この修正を適用してみましょう
これで このメソッドは メインアクターに分離されました ビルドしてみると 成功します
成功した理由を確認するために 呼び出し元を簡単に見てみましょう
2回呼び出されていて 2回とも メインアクターにあるメソッドから 呼び出されています メインアクター以外の場所で この関数が呼び出されていたなら そのことを通知する コンパイラエラーが発生し その呼び出し元を調べて 呼び出し時の コンテキストを確認できたでしょう 1つはSwiftUIビューです もう1つは WKApplicationDelegateの実装内です こちらを見てみましょう
WKApplicationDelegateを Optionキーを押しながらクリックすると メインアクターに関連づけられた プロトコルであることがわかります これにより メインアクターでのみ 呼び出されることが保証されます WatchKitフレームワーク経由か Swift 6モードを有効にしたあと コード経由で呼び出されます
メインアクターでのみ 動作するように設計された デリゲートや SwiftUIビューなどのほかのプロトコルは このような注釈がよくついていますが Xcode 16に付属する 最新のSDKでは特にそうです そして何よりも これには SwiftUI Viewプロトコルが含まれています これまでは 厳密な 並列処理チェックを有効にした場合 新しいSDKで必要となるよりも多くの メインアクターの注釈を追加する必要があり そうした注釈の一部を 削除できることもありました 次に デリゲートコールバックと 並列処理について少しお話ししましょう ご存じと思いますが デリゲートまたは完了ハンドラから コールバックを受信する時は常に そのコールバックでの並列処理について どのような保証があるかを 最初に把握する必要があります 一部のコールバックには保証があり すべてのコールバックが 常にメインスレッドで実行される旨が ドキュメントに 記載されている場合があります 多くのUIフレームワークが この保証を提供しています そうした理由もあり コールバックを メインアクターとしてマークしている場合 Watch Extensionのビューレイヤーに それほど多く警告が表示されません
一方 デリゲートの一部ではその逆で コールバックの方法について保証はなく 任意のスレッドやキューで実行されると 記載されています この場合に大きなメリットを得られるのは アプリのバックエンドで 処理されることが多いコールバックです CoffeeTrackerでHealthKitから受け取る コールバックは 主にこのタイプです そのような場合 ユーザーは適切なキューや アクターに再ディスパッチするか スレッドセーフな方法で 処理を行う必要があります この方法の問題点は ドキュメントでしか確認できないルールが 各デリゲートにあり 適切な処理を行うために ユーザーに大きな負担がかかることです コールバックがどこで行われるのかや ロジックの次の部分をどこで行うのかを ユーザーが考えなくてはなりません そして 確認を忘れたり 適切な場所への再ディスパッチを 忘れたりすると 簡単にデータ競合が発生しかねません さらに悪い状況として コールバックが すでに配置され動作しており たまたま いつもメインキューで実行されていただけで 必ずしもそれが 保証されているわけではないとしましょう その後 このフレームワークに 何か変更が加えられると 別のキューでコールバックが 実行されることになります しかし UIレイヤーはこのコールバックの メインキューでの実行を前提としています ここで欠けているのは ローカルな推論です これは UIレイヤーで作業している時に アプリ内のほかの場所のコードで 処理を行うキューを変えるような 変更が加えられても 簡単には齟齬が生じないという保証です
Swiftの並列処理では この問題に対処するために こうした保証が行われるか または保証がないことが明示されます あるコールバックについて コールバックの方法が指定されていない場合 非分離と見なされ 特定の分離を必要とする データにはアクセスできません 一方 コールバックで分離が保証される場合 例えば 常に メインアクターでコールバックされる場合 そのデリゲートプロトコル またはコールバックについて 常にメインアクター上にあると 注釈をつけることができるので コールバックの受け取り側では その保証により 懸念を解消できます それでは 最初に見た警告に戻りましょう メインアクター上のデリゲートの型が nonisolatedプロトコルに準拠していない というものです ここでは いくつかの方法を コンパイラが提案しています 1つ目は メソッドを nonisolatedとして宣言する方法です これにより このメソッドは メインアクターに分離された型の メソッドであるにも関わらず メインアクターに分離されなくなります これは コールバックの実行場所について 意図的に保証しないコールバックに 適した方法です これはビューなので 当然ながら すぐにでもメインアクターの作業を 始めたいところです そうしないと このコードをコンパイルする時に 新しいエラーが発生します メインアクターによって保護されている ビューのプロパティに アクセスしているからです
これを修正するには メインアクターでタスクを開始します
ただし この場合のコールバックは メインアクター上にあると想定されていて メインアクターで実行されている CoffeeKit内のモデルの型から 送られてくることがわかっています
コードベース全体を保守している場合に 使用できる 1つの方法は 今すぐ修正することです 定義に移動すると このように CoffeeKit内にプロトコルがあります @MainActorの注釈をつければ メインアクターで呼び出されることを 保証できます しかし 誰もがコードベース全体を 保守しているとは限りません 別のチームが CoffeeKitを保守している場合や ほかの担当者が保守するフレームワークや パッケージを利用している場合もあります 今回はこれが当てはまるとして デリゲートの実装に戻りましょう
さて このメソッドはメインアクターで 呼び出されるとわかっています コードを確認済みであるか このデリゲートのドキュメントで 確かめたのかもしれません
特定のアクターで呼び出しが行われると 確実にわかっている場合は それをコンパイラに知らせるための 方法があります assumeIsolatedというものです
コードの先頭をTaskにする代わりに MainActor.assumeIsolatedと記述します これで 新しいタスクをメインアクターで 非同期で開始することなく メインアクターでコードが実行されていると Swiftに知らせるだけになります
コードの一部がメインアクター以外から この関数を呼び出す可能性は 依然として十分にあることに 注意してください これは メインスレッドから 呼び出されることを前提とする 現在のSwift関数と同じです これを防ぐ方法は その関数が実際に メインアクターにあるというアサーションを 関数内に追加するというものですが それこそがassumeIsolatedの機能です この関数の呼び出し元が メインアクターでなかった場合 トラップが発動し プログラムの実行が停止します トラップの使用は望ましくありませんが 競合状態によって ユーザーデータが破損するよりはましです
メインアクターでの呼び出しを前提とする デリゲートプロトコルに準拠させる このパターンは一般的であるため もっと簡単な方法が用意されています そこで 変更を元に戻し
代わりに プロトコルへの準拠について @preconcurrencyと記述しましょう
これは 先ほど記述したことの すべてに相当します このビューの分離先であるアクターで 呼び出されていると想定し そうでない場合トラップを発動します
並列処理に関する警告を すべて解消したので このターゲットで Swift 6を有効にする準備ができました 設定に移動します 今度はSwift言語モードを検索します
Swift 6に設定します
コンパイルします エラーや警告なしでビルドできます データ分離の完全なチェックが Extensionに組み込まれました 今後は変更を加えても データ分離の完全なチェックが コンパイラで実行されるので データ競合を 誤って発生させることはありません これで ExtensionがSwift 6モードになったので 続いて CoffeeKitターゲットに注目しましょう 今度はこのターゲットに取り組みます @MainActorの注釈を デリゲートプロトコルに追加しましょう 検索して メインアクターの注釈を追加し 再ビルドすると 新しい警告が表示されます
警告は再びExtension内であり 先ほど追加した @preconcurrency属性に関するものです
このプロトコルがメインアクター上に あることが保証されていると コンパイラが認識できるようになったため @preconcurrency属性は必要なくなった と警告しています では削除しましょう
これで この問題は解決したので 先ほどと同じ手順に従って 並列処理の完全なチェックを有効にします プロジェクトの設定に移動し
CoffeeKitターゲットで チェックを有効にします
ビルドします
今度は警告が増えて 11個表示されています ごくシンプルなプロジェクトでも 結構な数の警告が出ます
この程度の警告はよくあることです 並列処理の完全なチェックを プロジェクトで有効にすると 数百や数千もの警告が 生成されることもあります 何か大変なことをしたのではと 心配になるかもしれませんが 慌てないことが大事です
ちょっとした問題が原因で 大量の警告が 表示されるのは珍しくありません そして多くの場合 こうした問題はすぐに修正できます
並列処理の警告を 最初に片づける時は メソッドをメインアクターに配置する グローバル変数をイミュータブルにするなど いくつかの簡単な変更により このような多数の警告をすばやく減らせます おすすめの戦略は 厳密なチェックを初めて有効にした時は すぐに成果を得られる点を見つけて対処し まず最も簡単な修正で 警告を減らすことです 多くの問題の根本原因となっている問題を 探すことも大切です 場合によっては 1つの行を変更するだけで 関連する数百の問題を解決できます 以前のバージョンのXcodeで 完全なチェックを試している場合は 最新のXcodeベータ版を 試してみる価値もあります 新しいSDKには 移行に役立つ 追加の注釈が含まれているからです 例えば 現在はすべてのSwiftUIビューが メインアクターに関連づけられているため MainActorの注釈を ビューの型に自分で追加する 必要がなくなりました 実際 こうした注釈は削除できる場合があります すでに推論されているからです
それが完了すると たいていは 対処が難しい警告の数が 大幅に減っています もう1つのポイントは すべての問題に一度に対処する 必要はないということです リリースを公開する必要がある場合や 差し迫った変更を行う必要がある場合は 設定に戻って 厳密なチェックを再び無効にできます これらの警告を減らすために行った すべての変更は コードベースに対する 有効な改善となるので 保存しておけば 一時的にチェックを 最小限に戻した場合でも 後から確認できます 後ほど準備ができたら この設定に戻り 改めて対処できます
今回のケースでは 警告を確認してみると 先ほど見たパターンがあるのがわかります これらのグローバル変数のいくつかは varとしてマークされていますが これらはすべて定数であるか ミュータブルである必要のないものです 先ほど見たloggerと同様です これらはすべて簡単に解決できます ちなみにこれは 複数行編集のスキルが役立つ場面です
varをハイライトし
Cmd+Option+Eキーですべて選択して
letに変更します
ビルドします
これで これらの警告が消えます
以上の例では 実際より簡単に見せるために わざと楽なものを用意したのではありません これはあくまでサンプルプロジェクトで 実際のプロジェクトでは より多くの警告が表示されます しかし 私たちの経験では 実際の本格的なプロジェクトでも これはよくあることで 簡単に解決できるものが大半であり それが片づけば 残りの困難な問題は少数です 最後のエラーを見てみましょう これらの問題の原因は 様々なアクター間で Drinkの配列を渡していることです 例えば この1つ目が示しているのは self.currentDrinksを このsaveメソッドに送ると データ競合が生じ得るということです saveは別のアクターの メソッドであることに注意してください CoffeeDataはメインアクターにあります これはSwiftUIの ObservableObjectなので そうである必要があります
一方 saveは別のアクター上 つまり
このCoffeeDataStoreアクター上 にあります このアクターはバックグラウンドで ディスクからの保存と読み込みを行います 警告に戻ると メインアクターに分離された モデル内に保持しているDrinkの配列と 同じものを saveに送信していることがわかります
Drinkが参照型の場合 メインアクターと保存アクターの両方が 共有されたミュータブルステートに 同時にアクセスできるため データ競合が発生する可能性があります この問題に対処するために Drinkを見てみましょう 定義に移動してみると
構造体であることがわかり イミュータブルなプロパティがありますが それらはすべて値型です これに基づいて判断すると 明らかにSendableにすることができ まったく問題なく Drinkをあるアクターに保存してから その同じ配列を 別のアクターに送信できます
これがinternalの型であった場合は Swiftは自動的に この型をSendableと見なします しかし これはpublicの型なので CoffeTracker Extensionを使用して CoffeeKitの外部で共有します
Swiftでは publicの型がSendableかどうかは 自動的に推論されません これが行われない理由は 型をSendableとしてマークすることは クライアントへの保証になるからです 現時点では この型に ミュータブルステートはありませんが 後から変更したくなるかもしれないので Sendableかどうかを 焦って確定したくはありません そのため Swiftでは publicの型には Sendableへの準拠を 明示的に追加する必要があります
この場合もそうしたいと思います ちなみにこれは 1つの行を変更するだけで 複数の警告を一度に解消できる例です 3か所で Drinkの型を Sendableに変更する必要があります 大規模なプロジェクトでは3つでは済まず 複数のプロジェクトで 数十の警告が発生することもあります それでは この型をSendableとしてマークしましょう
再コンパイルします
すると Sendableではない 別の型があることがわかります
これは単なる列挙型なので Sendableとしてマークすることもできます しかし そうでない場合 例えば Objective-C型で ミュータブルステートを 参照型に格納するため Sendableではない型であった場合は どうでしょうか この時点で安全性について 選択をする必要があるかもしれません 1つの方法は その型について推論し それがミュータブルな参照型であっても 例えば NSCopyingで生成された 新しいコピーなので 安全だと見なすことです 考えられる方法としては Sendableではなくても そのクラスを保護し 例えば変数をprivateにして Sendableな型に格納することもできます そうするために この場合も nonisolated(unsafe)キーワードを 使えます
これを行うと Sendableの注釈を使用して Drinkの型がコンパイルされます 今回はこの必要はないので 変更を元に戻し
代わりに DrinkTypeに移動して
これもSendableとしてマークします
CoffeeKitでの並列処理に関する警告を すべて解消したので このターゲットで Swift 6モードを有効にできます もう一度設定に移動します
今度はSwift言語モードを検索します
Swift 6に設定します
コンパイルします
ビルドできます この時点で CoffeeTrackerアプリ全体が Swiftの並列処理で保護されています
保護されるようになったので CoffeeTrackerに 新しい機能を追加してみましょう ユーザーが求めているのは コーヒーを飲んだ時の位置情報を追跡し そのデータを活用して カフェイン摂取習慣について 役立つ情報を得ることです そこで CoreLocationをアプリに追加します では CoffeeKitのaddDrinkメソッドに 移動しましょう Drinkを追加する直前に CoreLocationを使用して ユーザーの現在位置を取得します CoreLocationには 現在位置を ストリームする非同期シーケンスがあります これは Swiftの並列処理に適しています ここでは その機能を使い 位置情報の結果をループ処理して 適切な水準の精度が得られた時点で カフェインのサンプルに 位置情報を割り当てることができます このコードには タイムアウトを 追加したほうがいいと思いますが これには Swiftの 構造化された並列処理とキャンセルで 対応できます この方法には1つだけ問題があります
CoffeeTrackerの最小の配備ターゲットを 引き上げる必要があるのです そして その準備がまだできていません 一部のユーザーは watchOS 10にまだ更新したくないけれど コーヒーをどこで飲んでいるか 追跡したいと考えています そのため デリゲートコールバックに基づく 古いCoreLocation APIを 代わりに使用する必要があります これはSwiftの並列処理より前のものなので 使うのに少し技巧を要します 先ほど紹介した新しい方法の 本当に優れた点は このコードが 通常の同期コードのように見えることです 位置情報を要求して 位置情報更新の受信ストリームをループ処理し 十分な精度が得られるまで 反復するというプロセスを すべてこの関数内で行い 新しいDrinkを配列に追加できます 一方 デリゲートAPIでは 何らかのステートを保存しておき デリゲートメソッドが作動するのを待ち 作動したあと さらに Drinkの値を 位置情報とともに保存する必要があります したがって 新しいAPIを活用できるように 配備ターゲットを引き上げる価値はあります
しかし それを望まないとしましょう そこで この新しいコードを削除し
このファイルの一番下まで移動して
CoreLocationデリゲートオブジェクトを 作成します
これはデリゲートクラスの基本的な実装で CoreLocationから 位置情報の更新を受け取ることができます CoreLocationのこの部分は Swiftの並列処理よりも前のものです そのため ルールを守れるように さらに作業が必要になります 今回の解説でご紹介した ほかのコールバックとは このCLLocationデリゲートは少し異なります どのスレッドで呼び出されるかについて 静的な保証がありません
ここまで説明してきたのは 常に メインスレッドで呼び出されるデリゲートか 任意のスレッドで呼び出される デリゲートでした CoreLocationManagerの ドキュメントを見ると このデリゲートが呼び出されるスレッドは CLManagerを作成したスレッドで決まると 保証されていることがわかります これは動的なプロパティであり Swiftで自動的に適用するには 手を加える必要があります
今回の場合は メインアクターから モデルの型でこの情報を使っています したがって 最も簡単な方法は このデリゲートも メインスレッドに配置することです Swiftでそれを適用できるようにする モードに戻すのは簡単です この型を完全にメインアクターに配置します
これにより 位置情報マネージャは メインスレッドで作成されるようになります
場所はイニシャライザ内です そしてデリゲートも メインアクターに配置されることになります
もちろん これを行うと おなじみのエラーが コンパイラから表示されます このデリゲートコールバックは メインアクターに分離されていないからです これに対処する方法のパターンは すでに見てきました
デリゲートを nonisolatedとしてマークします
次に メインアクターで 実行する必要があるコードを MainActor.assumeIsolatedの 呼び出しでラップします
これでビルドが成功します Swift 6で構築しながら アプリで現在の位置情報を 取得できるようになりました 今回はSwift 6の言語モードに アプリを移行するための 手法をいくつか簡単にご紹介しました ここでは取り上げなかった シナリオが多数ありますが ご利用いただけるリソースが ほかにもあります 最初にご覧いただきたいのは 過去のセッションです Swiftの並列処理機能で 既存のアプリを最新の状態にする方法に ついて説明しています このコードをSwift 6に移行するための プロセスの大部分が その解説で取り上げたリファクタリングで より簡単になりました 過去の移行から得た教訓に基づいて Swift 6の言語モードへの移行は Swiftエコシステム全体で 段階的に進められるようになっています つまり コードの変更を1つずつ行い データ競合を排除することができます 静的データ競合の安全性と 動的データ競合の安全性の間を 橋渡しする手法について 今回少しだけお話ししましたが 私たちはコミュニティとして この移行に取り組んでいます これらすべての戦略と その他の内容をまとめたガイドを Swift.org/migrationでご覧いただけます ご自身のコードを作成する際に これらのリソースがお役に立てば幸いです ご視聴ありがとうございました
-
-
9:08 - Recaffeinater and CaffeineThresholdDelegate
//Define Recaffeinator class class Recaffeinater: ObservableObject { @Published var recaffeinate: Bool = false var minimumCaffeine: Double = 0.0 } //Add protocol to notify if caffeine level is dangerously low extension Recaffeinater: CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
9:26 - Add @MainActor to isolate the Recaffeinator
//Isolate the Recaffeinater class to the main actor @MainActor class Recaffeinater: ObservableObject { @Published var recaffeinate: Bool = false var minimumCaffeine: Double = 0.0 }
-
9:38 - Warning in the protocol implementation
//warning: Main actor-isolated instance method 'caffeineLevel(at:)' cannot be used to satisfy nonisolated protocol requirement public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } }
-
9:59 - Understanding why the warning is there
//This class is guaranteed on the main actor... @MainActor class Recaffeinater: ObservableObject { @Published var recaffeinate: Bool = false var minimumCaffeine: Double = 0.0 } //...but this protocol is not extension Recaffeinater: CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
12:59 - A warning on the logger variable
//var 'logger' is not concurrency-safe because it is non-isolated global shared mutable state; this is an error in the Swift 6 language mode var logger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView", category: "Root View")
-
13:38 - Option 1: Convert 'logger' to a 'let' constant
//Option 1: Convert 'logger' to a 'let' constant to make 'Sendable' shared state immutable let logger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView", category: "Root View")
-
14:20 - Option 2: Isolate 'logger' it to the main actor
//Option 2: Annotate 'logger' with '@MainActor' if property should only be accessed from the main actor @MainActor var logger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView", category: "Root View")
-
14:58 - Option 3: Mark it nonisolated(unsafe)
//Option 3: Disable concurrency-safety checks if accesses are protected by an external synchronization mechanism nonisolated(unsafe) var logger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView", category: "Root View")
-
15:43 - The right answer
//Option 1: Convert 'logger' to a 'let' constant to make 'Sendable' shared state immutable let logger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView", category: "Root View")
-
17:03 - scheduleBackgroundRefreshTasks() has two warnings
func scheduleBackgroundRefreshTasks() { scheduleLogger.debug("Scheduling a background task.") // Get the shared extension object. let watchExtension = WKApplication.shared() //warning: Call to main actor-isolated class method 'shared()' in a synchronous nonisolated context // If there is a complication on the watch face, the app should get at least four // updates an hour. So calculate a target date 15 minutes in the future. let targetDate = Date().addingTimeInterval(15.0 * 60.0) // Schedule the background refresh task. watchExtension.scheduleBackgroundRefresh(withPreferredDate: targetDate, userInfo: nil) { //warning: Call to main actor-isolated instance method 'scheduleBackgroundRefresh(withPreferredDate:userInfo:scheduledCompletion:)' in a synchronous nonisolated context error in // Check for errors. if let error { scheduleLogger.error( "An error occurred while scheduling a background refresh task: \(error.localizedDescription)" ) return } scheduleLogger.debug("Task scheduled!") } }
-
17:57 - Annotate function with @MainActor
@MainActor func scheduleBackgroundRefreshTasks() { scheduleLogger.debug("Scheduling a background task.") // Get the shared extension object. let watchExtension = WKApplication.shared() // If there is a complication on the watch face, the app should get at least four // updates an hour. So calculate a target date 15 minutes in the future. let targetDate = Date().addingTimeInterval(15.0 * 60.0) // Schedule the background refresh task. watchExtension.scheduleBackgroundRefresh(withPreferredDate: targetDate, userInfo: nil) { error in // Check for errors. if let error { scheduleLogger.error( "An error occurred while scheduling a background refresh task: \(error.localizedDescription)" ) return } scheduleLogger.debug("Task scheduled!") } }
-
22:15 - Revisiting the Recaffeinater
//This class is guaranteed on the main actor... @MainActor class Recaffeinater: ObservableObject { @Published var recaffeinate: Bool = false var minimumCaffeine: Double = 0.0 } //...but this protocol is not //warning: Main actor-isolated instance method 'caffeineLevel(at:)' cannot be used to satisfy nonisolated protocol requirement extension Recaffeinater: CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
22:26 - Option 1: Mark function as nonisolated
//error: Main actor-isolated property 'minimumCaffeine' can not be referenced from a non-isolated context nonisolated public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } }
-
23:07 - Option 1b: Wrap functionality in a Task
nonisolated public func caffeineLevel(at level: Double) { Task { @MainActor in if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
23:34 - Option 1c: Explore options to update the protocol
public protocol CaffeineThresholdDelegate: AnyObject { func caffeineLevel(at level: Double) }
-
24:15 - Option 1d: Instead of wrapping it in a Task, use `MainActor.assumeisolated`
nonisolated public func caffeineLevel(at level: Double) { MainActor.assumeIsolated { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
25:21 - `@preconcurrency` as a shorthand for assumeIsolated
extension Recaffeinater: @preconcurrency CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
26:42 - Add `@MainActor` to the delegate protocol in CoffeeKit
@MainActor public protocol CaffeineThresholdDelegate: AnyObject { func caffeineLevel(at level: Double) }
-
26:50 - A new warning
//warning: @preconcurrency attribute on conformance to 'CaffeineThresholdDelegate' has no effect extension Recaffeinater: @preconcurrency CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
27:09 - Remove @preconcurrency
extension Recaffeinater: CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
29:56 - Global variables in CoffeeKit are marked as `var`
//warning: Var 'hkLogger' is not concurrency-safe because it is non-isolated global shared mutable state private var hkLogger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.HealthKitController", category: "HealthKit") // The key used to save and load anchor objects from user defaults. //warning: Var 'anchorKey' is not concurrency-safe because it is non-isolated global shared mutable state private var anchorKey = "anchorKey" // The HealthKit store. // warning: Var 'store' is not concurrency-safe because it is non-isolated global shared mutable state private var store = HKHealthStore() // warning: Var 'isAvailable' is not concurrency-safe because it is non-isolated global shared mutable state private var isAvailable = HKHealthStore.isHealthDataAvailable() // Caffeine types, used to read and write caffeine samples. // warning: Var 'caffeineType' is not concurrency-safe because it is non-isolated global shared mutable state private var caffeineType = HKObjectType.quantityType(forIdentifier: .dietaryCaffeine)! // warning: Var 'types' is not concurrency-safe because it is non-isolated global shared mutable state private var types: Set<HKSampleType> = [caffeineType] // Milligram units. // warning: Var 'miligrams' is not concurrency-safe because it is non-isolated global shared mutable state internal var miligrams = HKUnit.gramUnit(with: .milli)
-
30:19 - Change all global variables to `let`
private let hkLogger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.HealthKitController", category: "HealthKit") // The key used to save and load anchor objects from user defaults. private let anchorKey = "anchorKey" // The HealthKit store. private let store = HKHealthStore() private let isAvailable = HKHealthStore.isHealthDataAvailable() // Caffeine types, used to read and write caffeine samples. private let caffeineType = HKObjectType.quantityType(forIdentifier: .dietaryCaffeine)! private let types: Set<HKSampleType> = [caffeineType] // Milligram units. internal let miligrams = HKUnit.gramUnit(with: .milli)
-
30:38 - Warning 1: Sending arrays in `drinksUpdated()`
// warning: Sending 'self.currentDrinks' risks causing data races // Sending main actor-isolated 'self.currentDrinks' to actor-isolated instance method 'save' risks causing data races between actor-isolated and main actor-isolated uses await store.save(currentDrinks)
-
32:04 - Looking at Drink struct
// The record of a single drink. public struct Drink: Hashable, Codable { // The amount of caffeine in the drink. public let mgCaffeine: Double // The date when the drink was consumed. public let date: Date // A globally unique identifier for the drink. public let uuid: UUID public let type: DrinkType? public var latitude, longitude: Double? // The drink initializer. public init(type: DrinkType, onDate date: Date, uuid: UUID = UUID()) { self.mgCaffeine = type.mgCaffeinePerServing self.date = date self.uuid = uuid self.type = type } internal init(from sample: HKQuantitySample) { self.mgCaffeine = sample.quantity.doubleValue(for: miligrams) self.date = sample.startDate self.uuid = sample.uuid self.type = nil } // Calculate the amount of caffeine remaining at the provided time, // based on a 5-hour half life. public func caffeineRemaining(at targetDate: Date) -> Double { // Calculate the number of half-life time periods (5-hour increments) let intervals = targetDate.timeIntervalSince(date) / (60.0 * 60.0 * 5.0) return mgCaffeine * pow(0.5, intervals) } }
-
33:29 - Mark `Drink` struct as Sendable
// The record of a single drink. public struct Drink: Hashable, Codable, Sendable { //... }
-
33:35 - Another type that isn't Sendable
// warning: Stored property 'type' of 'Sendable'-conforming struct 'Drink' has non-sendable type 'DrinkType?' public let type: DrinkType?
-
34:28 - Using nonisolated(unsafe)
nonisolated(unsafe) public let type: DrinkType?
-
34:45 - Undo that change
public let type: DrinkType?
-
35:04 - Change DrinkType to be Sendable
// Define the types of drinks supported by Coffee Tracker. public enum DrinkType: Int, CaseIterable, Identifiable, Codable, Sendable { //... }
-
36:35 - CoreLocation using AsyncSequence
//Create a new drink to add to the array. var drink = Drink(type: type, onDate: date) do { //error: 'CLLocationUpdate' is only available in watchOS 10.0 or newer for try await update in CLLocationUpdate.liveUpdates() { guard let coord = update.location else { logger.info( "Update received but no location, \(update.location)") break } drink.latitude = coord.coordinate.latitude drink.longitude = coord.coordinate.longitude } catch { }
-
38:10 - Create a CoffeeLocationDelegate
class CoffeeLocationDelegate: NSObject, CLLocationManagerDelegate { var location: CLLocation? var manager: CLLocationManager! var latitude: CLLocationDegrees? { location?.coordinate.latitude } var longitude: CLLocationDegrees? { location?.coordinate.longitude } override init () { super.init() manager = CLLocationManager() manager.delegate = self manager.startUpdatingLocation() } func locationManager ( _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] ) { self.location = locations. last } }
-
39:32 - Put the CoffeeLocationDelegate on the main actor
@MainActor class CoffeeLocationDelegate: NSObject, CLLocationManagerDelegate { var location: CLLocation? var manager: CLLocationManager! var latitude: CLLocationDegrees? { location?.coordinate.latitude } var longitude: CLLocationDegrees? { location?.coordinate.longitude } override init () { super.init() // This CLLocationManager will be initialized on the main thread manager = CLLocationManager() manager.delegate = self manager.startUpdatingLocation() } // error: Main actor-isolated instance method 'locationManager_:didUpdateLocations:)' cannot be used to satisfy nonisolated protocol requirement func locationManager ( _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] ) { self.location = locations. last } }
-
40:06 - Update the locationManager function
nonisolated func locationManager ( _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] ) { MainActor.assumeIsolated { self.location = locations. last } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。