【iOS】Swift6対応方法!Strict Concurrency Checking(厳密な並行性チェック)

【iOS】Swift6対応方法!Strict Concurrency Checking(厳密な並行性チェック)

この記事からわかること

  • Swift6対応方法
  • Strict Concurrency Checking(厳密な並行性チェック)
  • 有効にするには?
  • 警告エラー解消方法

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

環境

公式リファレンス:Swift 6へのアプリの移行 - WWDC24 - ビデオ

公式リファレンス:Migrating to Swift 6

Swift6からコンパイル時に検出できるように

公式リファレンス:Swift 6 アプリで厳密な並行性を採用する

Xcode16からSwift6モードのコンパイラが搭載されるようになりSwift6では厳密に並行性をチェックされるようになります。このチェックにより、「特定の再現手順の確立が容易でない場合もあるためデバッグが困難であったデータ競合の可能性」をコンパイル時に検出することが可能になりました。

Xcode16ではデフォルトではSwift6モードは有効になっていません。そのためXcodeをアップデート後に既存プロジェクトを開いても、新規プロジェクトを立ち上げても今までと変わらずSwift5モードで実行されます。

Swift6モードでのコンパイルエラーをチェックする

いきなりSwift6モードを有効にしてしまうとエラーが発生しビルドが通らなくなってしまうのでまずはコンパイルエラーになる箇所をチェックして、都度コードベースでの修正対応を行ってから有効にする流れをとります。

チェックするには「Build Settings」>「Swift Compiler - Upcoming Features」>「Strict Concurrency Checking(厳密な並行性チェック)」MinimalTargetedCompleteの3つから選択できるようになっているのでTargetedまたはCompleteに指定します。

公式リファレンス:Setting name: SWIFT_STRICT_CONCURRENCY

【iOS】Swift6対応方法!Strict Concurrency Checking(厳密な並行性チェック)

Minimal:デフォルト設定。Swift Concurrencyを利用している箇所かつ明示的にSendableを定義している箇所でのみチェック

Targeted:Swift Concurrencyを利用している箇所でのみチェック

Complete:Swift Concurrencyを利用していない箇所も含めてプロジェクト全体でチェック

これでビルドを行うと例えば以下のような警告が表示されるようになります。


Stored property 'プロパティ名' of 'Sendable'-conforming class 'クラス名' has non-sendable type 'データ型'; this is an error in the Swift 6 language mode
Main actor-isolated class property 'shared' can not be referenced from a nonisolated context; this is an error in the Swift 6 language mode

Swift6モードを有効にする

実際にSwift6モードに変更するには「Build Settings」>「Swift Compiler - Language」>「Swift Language Version」Swift6に設定するだけです。

【iOS】Swift6対応方法!Strict Concurrency Checking(厳密な並行性チェック)

Swift6に設定するとStrict Concurrency CheckingCompleteに設定され、警告ではなくコンパイルエラーに変わりビルドができなくなります。

対応方針

Swift6にアプリを対応させていくためにはスレッドセーフな設計にしていく必要があります。スレッドセーフな設計を実装する手段はいくつかあるので要件に応じた手段で対応を追加していきます。

スレッドセーフの設計にする手段

パターン1:UIレイヤーの対応

例えば以下のような「ローディング機能を管理するViewModel」などUIに直接絡むクラスであれば@MainActorを追加(メインスレッドでの操作に限定)するだけで警告が解消します。以下の場合はDispatchQueue.main.asyncAfterの部分で参照するselfに警告が発生します。

// MARK: 順番上下ローディング
@MainActor
final class LoadingViewModel: ObservableObject {

    @Published  private(set) var height: Double = 0
    @Published  private(set) var timerPublisher: AnyCancellable?
    
    public func onAppear() {
        startLoading()
    }
    
    public func onDisappear() {
        stopLoading()
    }
    
    private func startLoading() {
        timerPublisher = Timer.publish(every: 0.3, on: .main, in: .common)
            .autoconnect()
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in
                guard let self else { return }
                self.height += 15
                if self.height == 30 {
                    self.height = 0
                    self.timerPublisher?.cancel()
                    DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
                        // Task-isolated 'self' is captured by a main actor-isolated closure. main actor-isolated uses in closure may race against later nonisolated uses
                        guard let self else { return }
                        self.startLoading()
                    }
                }
            }
    }
    
    private func stopLoading() {
        height = 0
        timerPublisher?.cancel()
    }   
}

パターン2:actorでスレッドセーフ設計

スレッドセーフな設計にするためにはactorを使用します。actorデータの排他制御を行うことができるスレッドセーフなオブジェクトなので置き換えるだけで対応が完了します。と言いたいところですがactorに置き換えるとawaitを使用して呼び出す必要が出てくるので呼び出し側の改修範囲が大きくなってしまうデメリットもあります。

actor Score {
    private(set) var value: Int = 0

    func output(score: Int) {
        print("Score:\(score)")
    }
}
let score = Score()

Task {
    try await Task.sleep(nanoseconds: 1 * 1_000_000_000) // 1秒停止
    await score.update(score: 200)
    print("非同期処理1")
}

パターン3:@unchecked Sendableでスレッドセーフをコードベースで保証

対象のクラスにSendableをそのまま準拠できるのであればそれで良いですが準拠できない場合もあります。その際でもコードベースでスレッドセーフな設計になっている場合@unchecked Sendableを使用することでコンパイラに「スレッドセーフである」と認識させることで警告を抑制することも可能です。ただデータ競合が起こらないという保証はなくなるので場合によってはDispatchQueueなどを使用して正しくスレッドを管理する必要があります。

final class UserDefaultsRepository: @unchecked  Sendable {

    /// `UserDefaults`が`Sendable`ではないがスレッドセーフな設計
    private let userDefaults: UserDefaults = UserDefaults.standard

    /// Bool:保存
    public func setBoolData(key: String, isOn: Bool) {
        userDefaults.set(isOn, forKey: key)
    }

    /// Bool:取得
    public func getBoolData(key: String) -> Bool {
        return userDefaults.bool(forKey: key)
    }
}

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index