😬

[TCA]TCAを使ったアプリのSwift6対応でつまづいたこと

2024/10/29に公開

要約

TCAを使った個人開発アプリで、Swift6対応を行った。
Dependencyの実装でつまずいたので、問題とその解決策を共有する。

対応したアプリ

https://apps.apple.com/jp/app/randomfizzbuzz/id6642706096?l=en-US

環境

  • TCA v1.15.2
  • Xcode: v16.0
  • Swift: v6

実装

Soundの再生を行うSoundPlayerDependencyを実装していた。
以下のコードは、Swift5言語モードでは、コンパイルできる。

@DependencyClient
struct SoundPlayer: Sendable {
    var setup: () -> Void
    var play: () -> Void
}

extension DependencyValues {
    var soundPlayler: SoundPlayer {
        get { self[SoundPlayer.self] }
        set { self[SoundPlayer.self] = newValue }
    }
}

extension SoundPlayer: DependencyKey {
    // `setup`の中で初期化する。
    static var soundPlayer: AVAudioPlayer?
    static func live() -> Self {
        return Self(
            setup: {
                let audioSession = AVAudioSession.sharedInstance()
                try? audioSession.setCategory(.ambient)
                try? audioSession.setActive(true)

                soundPlayer = try? AVAudioPlayer(data: NSDataAsset(name: "OKSound")!.data)
                soundPlayer?.prepareToPlay()
            },
            play: {
                if soundPlayer?.isPlaying == true {
                    // To handle repeated-tap
                    soundPlayer?.currentTime = 0
                }
                soundPlayer?.play()
            }
        )
    }

    static let liveValue = Self.live()
}

発生したコンパイルエラー

Swift6言語モードを有効にすると以下のコンパイルエラーが発生した。

エラー1

struct SoundPlayerの中のsetupplayで、以下のエラーが表示される。

Stored property '_play' of 'Sendable'-conforming struct 'SoundPlayer' has non-sendable type '() -> Void'

原因

setupplaySendableでないから、エラーになる。

解決方法

setupplaySendableにする。

@DependencyClient
struct SoundPlayer: Sendable {
    var setup: @Sendable () -> Void
    var play: @Sendable () -> Void
}

エラー2

SoundPlayerstatic var soundPlayer: AVAudioPlayerを宣言している箇所で、以下のエラーが表示される。

Static property 'soundPlayer' is not concurrency-safe because it is nonisolated global shared mutable state

原因

soundPlayerはActorに隔離されておらず、どのThreadからもアクセスが可能になっている。そのため、複数のスレッドから同時に読み書きされる可能性がある。

解決方法1

soundPlayerletにする。書き込みされることがなくなるので、concurrency-safeになる。読むだけなら、複数のスレッドから同時にアクセスされてもデータ競合は発生しないため。

解決方法2

var soundPlayerfunc live()->Selfの中に移動させる。

 static func live() -> Self {
        let audioPlayer = try? AVAudioPlayer(data: NSDataAsset(name: "OKSound")!.data)
        return Self(
            setup: {
                let audioSession = AVAudioSession.sharedInstance()
                try? audioSession.setCategory(.ambient)
                try? audioSession.setActive(true)
                audioPlayer?.prepareToPlay()
            },
            play: { _ in
                if audioPlayer?.isPlaying == true {
                    // To handle repeated-tap
                    audioPlayer?.currentTime = 0
                }
                audioPlayer?.play()
            }
        )
    }
}

audioPlayerを関数内のlocal scopeにできるため、concurrency-safeになる。
また、関数内からはaudioPlayerにアクセスできるためsetup,playから参照できる。

参考にしたコード

isowordsのコードを見ると、解決方法2のようなコードが発見できる。(e.g. DictionaryClient, ApiClient)
解決方法1に比べて柔軟に対応できそう。

https://github.com/pointfreeco/isowords

所感

TCAはSwift6対応してくれているため、比較的簡単にSwift6移行できた。

References

Discussion