SwiftUI迁移至@Observable后Toggle双向绑定报错求助
我开发的App会在开关开启时播放确认提示音,为了能在App多处调用该逻辑,把开关状态存在SoundManager类里。现在正把代码从ObservableObject迁移到@Observable(原版本没报错),但构建新代码时,UtilView里的Toggle开关报错:
Cannot find '$sound' in scope
要是移除$符号,又会出现另一个报错:
Cannot convert value 'soundStatus' of type 'Bool' to expected type 'Binding', use wrapper instead. Insert $
两种操作都解决不了问题。之前针对这类绑定错误的方案(适用于@Observable之前的版本)建议用.wrappedValue或.enumerated(),但因为要在App多处检查开关状态,这些方案好像不适用。
触发确认提示音的代码
@Environment(SoundManager.self) private var sound if sound.soundStatus { sound.playSound(sound: .confirmTone) }
UtilView代码
struct UtilView: View { @Environment(SoundManager.self) private var sound var body: some View { VStack (alignment: .leading) { List { // menu here Toggle(isOn: $sound.soundStatus, // <– Error here label: { Text("App Sound Confirmations") } ) .toggleStyle(SwitchToggleStyle()) } } .listStyle(.plain) } }
SoundManager类代码
import AVKit import SwiftUI @Observable class SoundManager { // class SoundManager: ObservableObject { @AppStorage(StorageKeys.sound.rawValue) var soundStatus: Bool = false var player: AVAudioPlayer? var session: AVAudioSession = .sharedInstance() func playSound(sound: SoundOption) { guard let url = Bundle.main.url(forResource: sound.rawValue, withExtension: ".wav") else { return } do { try session.setActive(false) try session.setCategory(.playback) player = try AVAudioPlayer(contentsOf: url) player?.setVolume( 0.2, fadeDuration: 0) try session.setActive(true) player?.play() } catch let error { #if DEBUG print("Error playing sound. \(error.localizedDescription)") #endif } } }
这个问题的核心是@Observable类结合@AppStorage时,没法直接用$sound.soundStatus创建Binding——因为@AppStorage本身是属性包装器,@Observable的自动绑定逻辑对它不生效。下面给两种实用的解决办法:
办法1:直接在View里手动创建Binding
不用改SoundManager,直接在Toggle里写Binding的get和set逻辑:
Toggle(isOn: Binding( get: { sound.soundStatus }, set: { sound.soundStatus = $0 } ), label: { Text("App Sound Confirmations") })
这种方式简单直接,适合快速修复问题。
办法2:在SoundManager里封装绑定属性
如果多个View都要用到这个开关的绑定,可以在SoundManager里加一个计算属性,专门返回Binding:
@Observable class SoundManager { @AppStorage(StorageKeys.sound.rawValue) var soundStatus: Bool = false // 新增计算属性 var soundStatusBinding: Binding<Bool> { Binding( get: { self.soundStatus }, set: { self.soundStatus = $0 } ) } // 原有代码不变... }
然后在UtilView里直接用这个属性:
Toggle(isOn: sound.soundStatusBinding, label: { Text("App Sound Confirmations") })
这种方式更优雅,能减少重复代码,适合多处复用的场景。
补充说明
以前用ObservableObject时,@Published属性能通过$获取Binding,但@Observable是编译器自动生成观察逻辑,和@AppStorage的属性包装器逻辑冲突,所以没法自动生成$绑定。手动创建Binding是目前最稳妥的方案。
内容的提问来源于stack exchange,提问作者Galen Smith

