ドキュメントなど
Swift Migration Guide
Swift6への移行 Strict Concurrency入門
Swift6 新機能
Introduce Existential any
Cancellableなど、いくつかの型(プロトコル)を持った変数を宣言する際、Cancellableという型名の前に「any」をつけろと言われました。こちらは「Introduce Existential any」と呼ばれる新たな機能で、存在型(Existential Type)としてプロトコルを指定する場合は、「そのプロトコルに適合した何某かの型」という意味合いでanyマークをつけなければなりません。Existential Typeとは、変数の型や、メソッドに渡す引数の型や、ジェネリクス型の括弧に囲まれた型などを言います。
private var subscription: any Cancellable
func doSomething(param: any MyProtocol) { }
class MyClass<T: any MyProtocol> { }
Swift Concurrency
別記事を参照:
Swift Concurrency まとめ
ケーススタディ
Xcode16から、project > Build Settings > upcoming で検索すると、Swift6からの新しい警告が表示されるようになりました。YESにすると、有効化できます。サンプルプロジェクトで、全て有効化してみた結果、いくつかのエラーが出たので共有します。
Introduce Existential any
ライブラリSwiftGen で自動生成されるファイルについて、Introduce Existential any向けの対応がなされていないようで、警告が出てしまいました(2024/7現在)。このファイルは自動生成されるファイルですから、開発者側で勝手にいじるべきところではありません。SwiftGen側でこの件の対応がなされるまでは、「Existential any」の警告はOFFのままにしたほうがいいかもしれません。
Default Internal Imports
「Default Internal Imports」という警告をONにしたところいくつかコンパイルエラーとなりました。
これは、importにアクセスレベルを設定できる新機能のようです。例えば、UIKitなどを普通にimportしていると、internalレベルでのimportという扱いとなります。明示的に public importしていないことになるのに, にもかかわらずUIKitの中のクラスなどをpublic extensionで使おうとしておりエラーとなってしまいました。UIKitをpublic importに変更すると解決しました。この機能は、 importされたシンボルがpublic, privateなどどのような範囲からアクセスできるかというのを指定できる機能ということです。
import UIKit // This is an internal import
public extension UICollectionView {} // Error
public import UIKit
public extension UICollectionView {} // OK!
Strict Concurrency Checking
こちらはXcode14からあるようですが紹介いたします。Swiftの新機能であるConcurrency Checkingを有効化する設定です。これは、あるデータが同時に読み書きされた場合にデータ競合を発生させないよう、コンパイラの段階でチェックしてくれる機能です。
まだApple はSwift6対応を対応を強制している訳ではなさそうですので、Xcode16にしてもSwift5のままにしておくことは可能です。
しかし、この機能を有効化し、警告が出るような箇所は、Swift6に移行した場合にコンパイルエラーとなってしまう場合があります。試してみたところ、全てコンパイルエラーになるとは限らないようでした。皆さんが対応される場合は、一度試しにSwift6をオンにしてみて、コンパイルエラーになるような箇所から対応されることを勧めます。
さて、Concurrency Checkingは三段階存在します。
最も厳格なCompleteで警告が発生しなければ、Swift6にも安全に移行できるようです。ただ、現段階ではXcode16がBetaのため、警告が出る箇所が多少変動することもあるようです。
-
Minimal
Sendableに適合させたい型が、実際にはSendableに適合できない場合、警告が発生します。例えば、ストアド・プロパティを持つクラスはSendableに適合しません。※ Sendableとは、複数スレッドからアクセスされても、データ競合が発生しないことが保証されていることを表す新たなプロトコルです。
-
Targeted
Minimal設定のチェックに加えて、コンパイラは、Taskクロージャやasync letのようなSwiftの並行処理が使われている場合にも警告を出します。例えば、Taskクロージャは、Sendableプロトコルに適合していない変数を内部へキャプチャできません。そのため、コンパイラーはその箇所で警告を出すようになります。 -
Complete
Targeted 設定のチェックに加えて、コンパイラはデータ競合が起こる可能性のあるあらゆる場所で警告を出すようになります。例えば、DispacthQueue.main.async {}は、元のコンテキストとは異なる新しいスレッドを開始するため、データ競合を引き起こす可能性があり、警告が発生します。
移行戦略としては、いきなりSwift6に変更するのではなく、まずConcurrency Checkingを設定し、出た警告を全て解消できたら、Swift6に移行することを勧めます。ただ個人的な経験では、Swift6に移行してみて初めて生じるエラーもありましたので、早い段階で一時的にSwift6に変更してみるのも助けになりそうです。
Concurrency CheckingについてはいきなりCompleteでもいいと思いますが、Completeにしてみてあまりに警告が多すぎて時間がかかりすぎる場合は、まずはTargetedでもいいと思います。
エラー種別
公式ドキュメント的には起きるエラーが主に三種類あるようです。
グローバル変数、Static 変数関連
グローバル変数、Static 変数関連はプログラム上のさまざまなところからアクセス可能なものなので、データ競合の可能性に常に晒されており、宣言しただけでエラーが出ることがある。公式ドキュメントにも真っ先に記載されている。
Sendableな型の場合(例えばint型)、以下で対処できる。
- @MainActorをつけることで隔離ドメインの種類を変更する。
- 変数ではなく、定数とする。
- 普通の変数ではなく、計算変数とする。
- DispatchQueueなどコンパイラからは検知できない方法でスレッドセーフにしてある場合は、nonisolated(unsafe)をつけてエラーを解消する。
Sendableでない型の場合(典型的にはクラス)、さらに難しい。定数にしたとしてもエラーが解消しない。定数であったとしても、その型の内部の状態に複数箇所からアクセスすることで、データ競合が起きてしまい得るからである。Sendableに適合するか、@MainActorなどグローバルアクターをつけることとなる。
私見だが、今までグローバルな領域に何らかの複雑な機能を果たすクラスを宣言し、あちこちから利用していたような場合、かなり困ることになりそう。そもそもそのような運用をやめて、各利用箇所において一つずつそのクラスのオブジェクトを初期化して渡してあげたりすることで、static変数やグローバル変数をなくすことを考えなければならない可能性がある。ただ結構な工数となる可能性もある。
内部構造が比較的単純なクラスであれば、Sendableに適合しやすいのでしてもいいかもしれない。
主にUIレイヤーで使っているものであれば@MainActorをつけても良いかもしれない。
主にデータ層、ドメイン層で使っていたものであれば、@MainActorはつけれないので、他の解決策しかない。が、もしDispatchQueueを使うなどして排他制御を実装しておりすでにスレッドセーフなクラスであれば、単純にclassからactorに変えることで解決すると思われる(actorはSendableに適合しているため)。
プロトコル批准時における、プロトコル側と批准側のミスマッチ
ドキュメントにおいて、プロトコルの批准のミスマッチとして挙げられている種類のコンパイルエラーとなります。あるメソッドについて、プロトコルでは何も指定していないのでnonisolated状態となっていますが、このプロトコルを@MainActorのクラスが批准するとMainActor-isolatedとなってしまい、不適合となるのでこのようなエラーが発生することがあります。
ドキュメントでは、解決策としては以下のようなものが挙げられています。
- A プロトコル側に問題があるようなケース
- 1 元のプロトコル全部を@MainActor指定してしまう。
- 2元のプロトコルの一部だけ(特に、エラーが出ている特定のメソッドを)@MainActor指定してしまう。
- 3 元のプロトコルの一部(エラーが出ている特定のメソッド)にasyncをつける。
- @MainActorのメソッドにアクセスするときは暗黙的に非同期でアクセスすることになるので、asyncをつければ合致するようになり、エラーが解決するようである。
- 4 プロトコルに批准させるところの直前に@preconcurrency をつけて批准する。
- これで警告が消える。まだSwift Concurrencyに対応する予定のないプロジェクトで一時的に警告を消したい場合。
- B 批准側に問題があるようなケース
- 1 批准側で、エラーが出ている該当メソッドにnonisolatedマークを付与する。
- 例えば該当メソッドがmainactor-isolatedな他のプロパティにはアクセスしないので、あえてMainActor-isolatedにする必要がないような場合に使うと良い。
- 2 中間となるタイプを経由してプロトコルに批准するようにする。
- すでに目的のプロトコルを批准した中間クラスを用意し、この中間クラスを継承するようにするなど。ただ、構造が複雑になるきらいがある。
- 1 批准側で、エラーが出ている該当メソッドにnonisolatedマークを付与する。
私見ですが、そのプロトコルが主にUI層で使われるのか、あるいはその他の層でも使われるべきものなのか、エラーが出ている部分のメソッドなどが内部状態を変更しデータ競合を発生させ得るものなのか、といったことを考慮することが本質的な解決につながると思われます。
隔離境界の侵犯
ドキュメントでCrossing Isolation Boundariesとして挙げられているものとなります。
隔離境界(Isolation Domain)については別記事で解説しております。
ある隔離ドメインで宣言された、Sendableに適合していない型を、別の隔離ドメインのコンテキストから参照しようとするとエラーになります。ドキュメントでは以下の対策が紹介されています。
- その型をSendableに適合させる
- Sendableに適合させるのは骨が折れる場合がある。代わりに、sendingというマークもあり、これをつけられればエラーが解消できる。sendingは関数の引数につけることができる。引数として渡す値が、呼び出し元でその後一切いじられないことがプログラムの流れの中で保証されていれば、sendingマークをつけることが可能となる。 - その型がライブラリなどで宣言されており自分でいじれない場合は、そのライブラリをimportする際に@preconcurrency importとしてとりあえずエラーを消す(そのライブラリ側で対応をしてくれたあとは、@preconcurrencyを消す)
- 別のアプローチで、呼び出し側の隔離ドメインを呼び出し元の隔離ドメインに合わせる。例えば、呼び出し元が@MainActorであれば、呼び出し側の関数も@MainActorにしてしまえば解決する。
- @MainActorの連鎖はUIスレッドで多数の処理が同期的に走ることを意味するので、心配になるかもしれないが、時間のかかる処理をやっている箇所にピンポイントでnonisolatedマークをつけておくことで対処ができる。 - Sendableでない型(例えば、
MyClass
)を関数の引数として渡しているなら、その代わりに、その型を計算して返却してくれるクロージャ(例えば、@sendable () -> MyClass
)を関数の引数とすれば、問題は解消する。
具体的な警告と解消例
ドキュメントにおいても、Swift Concurrencyでよくあるエラーと対応策が書かれているので参照ください。
Common Compile Errors
sending ‘self.viewModel’ risks causing data races: sending task-isolated ‘self.viewModel’ to nonisolated callee risks causing data races between nonisolated and main actor-isolated uses
隔離境界の侵犯と呼ばれるエラーです。
ViewModelをSwiftUIのviewのbodyプロパティの中から参照していたところ、「sending ‘self.viewModel’ risks causing data races: sending task-isolated ‘self.viewModel’ to nonisolated callee risks causing data races between nonisolated and main actor-isolated uses
」との警告が出ました。ViewModelの宣言のところに「@MainActor
」をつけたところ、解消しました。万一、ViewModelが複数箇所から参照された場合のデータ競合を警告する趣旨と思われます。
@MainActor
class ContentViewModel {
// do something
}
Call to main actor-isolated instance method “foo()” in a synchronous nonisolated context; this is an error in the Swift 6 language mode
あるメソッドfoo()がnonisolated隔離されておらず、データ競合が起こりうる状態)で使われているとの警告のようです。
foo()メソッドの定義されているクラスは@MainActorとなっておりこのメソッド自体はisolatedなのですが、呼び出し側がnonisolated状態であったようです。
以下のように、Taskで該当メソッドを囲むと解消しました。
Task { await foo() }
Taskで囲むことで、データ競合から守られ、非同期的に実行されることが保証されたので、解消されたようです。また、以下のように@MainActorを付与し、データ競合から守られる旨を明示することでも解消しました。
Task { @MainActor in foo() }
Static property ‘shared’ is not concurrency-safe because non-’Sendable’ type ‘MyClass’ may have shared mutable state; this is an error in the Swift 6 language mode
以下のような警告も追加で出ていました。
Class ‘MyClass’ does not conform to the ‘Sendable’ protocol Annotate ‘shared’ with ‘@MainActor’ if property should only be accessed from the main actor Disable concurrency-safety checks if accesses are protected by an external synchronization mechanism
Sendableに適合していないクラス MyClass にstaticプロパティがある場合、データ競合が発生する可能性があると警告されています。
MyClassにSendableを付けてみました。
すると、さらに、クラスに 「final 」修飾子を付け、またMyClassのすべてのストアドプロパティの型もSendableにすべきだと警告が新たに出現します。
困った点が一つあり、このストアドプロパティの型のいくつかはライブラリMoyaの中で定義されたものでした。そのため、これらの型に自分でSendable適合性を後から追加することはできませんでした。そこで、「@preconcurrency
」マークを 「import Moya
」の部分に追加することで対処しています。このマークは、今のところ、Moyaに由来するクラスがSendableに適合していなくても、警告が出ないよう抑制してくれます。
また、さらに困った点として、ストアドプロパティがlazy var
である場合、単純には解決できませんでした。いくつか対処法がありえます。lazy var はデータ競合を引き起こす可能性があるので、もはや使用を諦めて、let を使って単純な定数とすることもできます。また、構造体を自分で定義した上で、シングルトンオブジェクトのような仕組みを実装することで、Sendable適合性を確保しつつlazyのような動きを維持できます。ここでは、そこまで実装するのが面倒でしたので、便宜上letを選びました。
最終的なコードは以下のようになりました。
Before:
import Moya
class MyClass {
static let shared = MyClass()
lazy var foo: Foo = {
return Foo()
}
}
After:
@preconcurrency import Moya
final class MyClass: Sendable {
static let shared = MyClass()
let foo: Foo = Foo()
}
Task-isolated value of type '() async throws -> ()' passed as a strongly transferred parameter; later accesses could race
以下のようなコードで発生しました。
func sendMessage(message: String, sender: MessageModel.Sender) {
do {
// 省略
Task {
let response = try await repository.request(
allMessages: allMessages,
sendMessage: message
)
latestMessage = response
messages.append(MessageModel(role: "model", message: response, sender: .server))
}
} catch {
self.error = error.localizedDescription
}
}
ViewModelでAPIを呼ぶ際にTask
クロージャを利用しています。そこでこのエラーが起きました。このエラーは、Task
クロージャ内部から、何かSendableに適合していない外部の変数を参照してしまっていることが原因で発生しています。
いくつか対策が考えられます:
-
参照されているものをActor, またはSendableとする。
-
参照されているものをキャプチャして、Immutableなコピーとしてから参照するようにする。
3.このメソッドをMainActorとする。 -
Taskの中からは参照しないようにする。
-
Taskそのものを使わないようにし、別のアプローチを考える。(多くはここまでしなくても解決できる。)
1は面倒臭い、2はImmutableにしてしまうとUIのアップデートができないという問題がありました。
3を採用しました。このメソッドはViewModelにあり、UIを更新するために使っており、@MainActorを使うのは適切と言えます。
@MainActor
func sendMessage(message: String, sender: MessageModel.Sender) {
do {
// 省略
Task {
let response = try await repository.request(
allMessages: allMessages,
sendMessage: message
)
latestMessage = response
messages.append(MessageModel(role: "model", message: response, sender: .server))
}
} catch {
self.error = error.localizedDescription
}
}
参考 Solving “Task-isolated value of type ‘() async -> Void’ passed as a strongly transferred parameter”
Discover how @MainActor works
Main actor-isolated instance method 'foo' cannot be used to satisfy nonisolated protocol requirement; this is an error in the Swift 6 language mode
「プロトコル批准のミスマッチ」系のエラーとなります。
Main actor-isolated property 'foo' can not be referenced from a non-isolated context; this is an error in the Swift 6 language mode
, Call to main actor-isolated instance method 'XXX' in a synchronous nonisolated context; this is an error in the Swift 6 language mode
など
どちらもMainActor隔離の変数などを非隔離のコンテキストから参照しようとした結果のエラーです。Taskで囲んでawait をつけて参照することとなります。