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 {}は、元のコンテキストとは異なる新しいスレッドを開始するため、データ競合を引き起こす可能性があり、警告が発生します。
具体的な警告と解消例
- 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() }