You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 19:10:04