macOS SwiftUI中List selection绑定值始终为nil的原因及解决方案
问题根因
- 自定义类型无法直接和UserDefaults交互:
UserDefaults仅支持存储基础类型、遵循NSSecureCoding的对象,或是经过Codable序列化后的Data数据。你的Week结构体虽然实现了Hashable/Equatable,但没有实现序列化能力,直接将Week?类型的值存入UserDefaults时,系统无法完成编码,实际存储的值始终为nil;同理启动时从UserDefaults读取初始值的逻辑,因为类型不匹配,as? Week的转换永远会失败,拿不到正确的初始值。 - 选中绑定逻辑存在双重冲突:在macOS的
SidebarListStyle下,当你给List传入全局selection绑定后,不需要再给每个子级的NavigationLink单独传入tag和selection参数。重复绑定同一个状态会触发写入竞争:用户点击侧边栏项时,List的全局selection逻辑会先把选中的week值写入绑定,而NavigationLink自身携带的selection逻辑,会在完成导航跳转后自动清空自己持有的绑定值,最终把已经写入的选中值覆盖为nil——这就是你观察到didSet触发、但值始终为nil的直接原因。 - 存储属性初始化时机不合理:你在类定义阶段直接把UserDefaults读取结果作为
selectedWeek的初始值,此时类的存储属性初始化流程还未完成,叠加类型不匹配的问题,初始值永远无法正常加载。
修复实现
第一步:为自定义模型补充序列化能力
给Week及关联的Day结构体添加Codable协议,让它可以被编码成Data存入UserDefaults:
struct Week: Identifiable, Hashable, Equatable, Codable { var id = UUID() var days: [Day] = [] var name: String } // 对应的Day结构同步添加Codable struct Day: Hashable, Codable { // 原有Day的属性保持不变 }
第二步:改造UserSettings的持久化逻辑
替换掉原来直接存取自定义类型的错误写法,改用序列化后的Data做持久化存储,同时在初始化阶段正确加载上次保存的选中状态:
class UserSettings: ObservableObject { @Published var weeks: [Week] = [ Week(name: "test week 1"), Week(name: "foobar"), Week(name: "hello world") ] @Published var selectedWeek: Week? { didSet { // 选中值变化时,编码为Data再写入UserDefaults guard let selectedWeek = selectedWeek, let encodedData = try? JSONEncoder().encode(selectedWeek) else { // 选中值为nil时,直接移除对应存储key UserDefaults.standard.removeObject(forKey: "week.selected") return } UserDefaults.standard.set(encodedData, forKey: "week.selected") } } init() { // 初始化时读取持久化的选中值 if let savedData = UserDefaults.standard.data(forKey: "week.selected"), let decodedWeek = try? JSONDecoder().decode(Week.self, from: savedData) { _selectedWeek = Published(initialValue: decodedWeek) } else { _selectedWeek = Published(initialValue: nil) } } }
第三步:简化List的绑定逻辑
移除每个NavigationLink上重复传递的tag和selection参数,只在NavigationLink上设置和选中类型匹配的tag,让全局List的selection绑定统一处理选中状态,消除绑定竞争:
struct MenuView: View { @EnvironmentObject var settings: UserSettings var body: some View { List(selection: $settings.selectedWeek) { ForEach(settings.weeks) { week in NavigationLink(destination: WeekView(week: week)) { Image(systemName: "circle") Text(week.name) } // 单独设置tag和List的selection类型匹配,完成选中状态联动 .tag(week) } .onDelete { set in settings.weeks.remove(atOffsets: set) } .onMove { set, i in settings.weeks.move(fromOffsets: set, toOffset: i) } } .navigationTitle("Weekplans") .listStyle(SidebarListStyle()) } }
补充注意:如果用户删除了当前选中的Week项,需要手动判断selectedWeek对应的id是否还存在于weeks数组中,如果不存在则将selectedWeek置为nil或数组中的第一个元素,避免出现无效的选中绑定。
内容的提问来源于stack exchange,提问作者Cravid
相关产品推荐
相关产品推荐

