[TCA]TCAを使ったアプリのSwift6対応でつまづいたこと
要約
TCAを使った個人開発アプリで、Swift6対応を行った。
Dependency
の実装でつまずいたので、問題とその解決策を共有する。
対応したアプリ
環境
- TCA v1.15.2
- Xcode: v16.0
- Swift: v6
実装
Soundの再生を行うSoundPlayer
Dependencyを実装していた。
以下のコードは、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
の中のsetup
とplay
で、以下のエラーが表示される。
Stored property '_play' of 'Sendable'-conforming struct 'SoundPlayer' has non-sendable type '() -> Void'
原因
setup
とplay
がSendable
でないから、エラーになる。
解決方法
setup
とplay
をSendable
にする。
@DependencyClient
struct SoundPlayer: Sendable {
var setup: @Sendable () -> Void
var play: @Sendable () -> Void
}
エラー2
SoundPlayer
のstatic var soundPlayer: AVAudioPlayer
を宣言している箇所で、以下のエラーが表示される。
Static property 'soundPlayer' is not concurrency-safe because it is nonisolated global shared mutable state
原因
soundPlayer
はActorに隔離されておらず、どのThreadからもアクセスが可能になっている。そのため、複数のスレッドから同時に読み書きされる可能性がある。
解決方法1
soundPlayer
をlet
にする。書き込みされることがなくなるので、concurrency-safeになる。読むだけなら、複数のスレッドから同時にアクセスされてもデータ競合は発生しないため。
解決方法2
var soundPlayer
をfunc 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に比べて柔軟に対応できそう。
所感
TCAはSwift6対応してくれているため、比較的簡単にSwift6移行できた。
Discussion