この記事について
私はジドスタというアプリを個人開発しており、先日のメジャーアップデートでStrict concurrency checking対応を行いSwift6に移行しました。
この記事はこの移行作業で行ったことや考えたことを整理して、備忘録もかねてまとめたものになります。
Strict Concurrency Checkingについて
Strict Concurrency Checkingは、ソースコードの並行処理をチェックしデータ競合を起こしうるコードを検出する仕組みです。これにより、従来は実行時に発生していたデータ競合を、コーディング中に静的に把握することができるようになりました。
いきなりSwift Language VersionをSwift6にすると、データ競合を起こしうるコードにエラーがつきビルドができなくなるので、Swift5のまま、Strict Concurrency CheckingをCompletedにして設定してエラーを警告として発生させ、それを解消していくという流れになるのですが、作業開始当初は私のアプリでは280個近くの警告が新たに発生しました。
対応方針
この警告達を解消にするはSwift Concurrencyなどを利用し、データ競合の恐れがないコードにリファクタリングする必要がありますが、私は以下のような方針で作業を行いました。
1. UIに関わる型や処理はMainActorに明示的に隔離する
私のアプリは非常にシンプルなGUIアプリで、ほとんどの処理がUIに関わるものです。なので、そもそも移行前の時点で既に多くの処理がメインスレッドで行われていたと考えたので、それらの処理をMainActorのIsolation domain内で行うように明示することにしました。
実際にこの作業で今回の警告の8割近くを解消しています。
2. 純粋なビジネスロジックを担当するモデルはSendable(Actor化など)にする
数は少ないのですがデータ削除など直接UIに関わらない処理や型があるのでそれらはAcotr化などを行い、移行後でも利用時にエラーが発生しないようにしました。
3. 現時点で対応が難しい箇所はMainActor.assumeIsolatedなどの暫定対応を行う
今回はひとまずSwift6への移行を完了させることを優先し、MainActorへの明示的な隔離ができず警告を解消できない場合などはMainActor.assumeIsolatedなどの暫定的な対応を行いました。
発生した警告と対処法の例
ケース1
警告
Conformance of 'AppDelegate' to protocol 'UNUserNotificationCenterDelegate' crosses into main actor-isolated code and can cause data races; this is an error in the Swift 6 language mode
発生箇所
extension AppDelegate: UNUserNotificationCenterDelegate { // 警告発生
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
// ユーザーがローカル通知に対してアクションを取った時の処理
}
対処法
UNUserNotificationCenterDelegateに@MainActorを付与
extension AppDelegate: @MainActor UNUserNotificationCenterDelegate
この警告はAppDelegateがMainActorに隔離されているのに、nonisolatedなプロトコルであるUNUserNotificationCenterDelegateを準拠させていることから、データ競合を恐れがあるというものです。
なので@MainActor UNUserNotificationCenterDelegateとして、このプロトコルへの準拠がMainActor上でしか行われないように制限することで警告を解消しました。
ケース2
警告
Conformance of 'TopViewController' to protocol 'WeightTableViewCellDelegate' crosses into main actor-isolated code and can cause data races; this is an error in the Swift 6 language mode
発生箇所
extension TopViewController: WeightTableViewCellDelegate { // 警告発生
func weightTableViewCellDidRequestKeyboardDismiss(_ cell: WeightTableViewCell) {
view.endEditing(true)
}
}
対処法
WeightTableViewCellDelegateの定義部分に@MainActorを付与
@MainActor
protocol WeightTableViewCellDelegate: AnyObject {
func weightTableViewCellDidRequestKeyboardDismiss(_ cell: WeightTableViewCell)
}
この警告もケース1と同じ警告で、MainActorで隔離されているクラスにnonisolatedなプロトコルを付与していることで発生しています。
ただ今回はWeightTableViewCellDelegateは自作プロトコルであり、これの要件の実行コンテキストも開発者が決めることができます。WeightTableViewCellDelegateの内容的にこれを準拠する側はViewControllerなどのMainActorに隔離されているオブジェクトであることが明らかなので、このプロトコル自体も@MainActorを付与することでプロトコルと準拠側とでコンテキストの違いが生まれないようにしました。
ケース3
警告
Capture of 'center' with non-Sendable type 'UNUserNotificationCenter' in a '@Sendable' closure
発生箇所
private func removeNotificationRequests() {
let center = UNUserNotificationCenter.current()
center.getPendingNotificationRequests { requests in
if !requests.isEmpty {
center.removeAllPendingNotificationRequests() // 警告発生
} else {
return
}
}
対処法
pendingNotificationRequestsのasyncバージョンを使用した
private func removeNotificationRequests() async {
let center = UNUserNotificationCenter.current()
let requests = await center.pendingNotificationRequests()
if !requests.isEmpty {
center.removeAllPendingNotificationRequests()
}
}
この警告はgetPendingNotificationRequests { requests in ... }のクロージャが@Sendableであるの対し、その中でSendableに準拠していないUNUserNotificationCenterをキャプチャしていることで発生しています。
removeNotificationRequests()はMainActorに隔離されているメソッドで、その中でcenterを定義しているのでcenterもMainActorに隔離されています。対して、getPendingNotificationRequests のコールバックのクロージャはバックグランドで実行されるので、centerがMainActorから他のIsolation DomainへとIsolation Boundaryを超えて渡されることになります。ですがcenter自体がSendableに準拠していないのでMainActorと渡された先のIsolation Domainとで並行にアクセスされた際にデータ競合を引き起こす可能性があり危険だとコンパイラは判断しているということだと私は考えています。
なので、removeAllPendingNotificationRequestsのasyncバージョンを使い、クロージャを使用しない形にして、そもそもcenterがIsolation Boundaryを超えないような構成にすることでこの警告に対処しました。
ケース4
警告
Main actor-isolated property 'weightTextField' can not be referenced from a nonisolated context
発生箇所
@IBOutlet weak var weightTextField: UITextField!
override func awakeFromNib() {
super.awakeFromNib()
weightTextField.keyboardType = .decimalPad // 警告発生
// その他のweightTextFieldの設定処理でも同様の警告が発生
}
対処法
MainActor.assumeIsolatedで囲んだ
@IBOutlet weak var weightTextField: UITextField!
override func awakeFromNib() {
super.awakeFromNib()
MainActor.assumeIsolated {
weightTextField.keyboardType = .decimalPad // 警告発生
// その他のweightTextFieldの設定処理
}
}
この警告はnonisolatedなメソッドであるawakeFromNib()内でMainActorに隔離されているweightTextFieldにアクセスしていることでコンテキストの不一致が起きていることが原因です。
awakeFromNib()は実態としてはメインスレッドで実行されますが、このことをコンパイラが理解できないために発生している警告なので、MainActor.assumeIsolatedを利用してひとまず暫定的な対応とすることにしました。
参考情報
Xcode 16 & Swift 6 キャッチアップ: Swift Concurrencyの基礎と最重要ポイントを総復習
(YouTube)
感覚的に理解するConcurrency: Swift 6はIsolationとSendableを用いてどのようにデータ競合を防止するか
(YouTube)
iOSDC Japan 2024: 座談会 「Strict ConcurrencyとSwift 6が開く新時代: 私たち… / shiz
(YouTube)
Swift6に向けて: Strict Concurrency Checking対応
Migrating to Swift 6