SwiftUI中@AppStorage与UserDefaults在init/didSet中失效的原因与解决
Date类型的Codable编码解码不稳定
JSONEncoder/Decoder默认对Date的编码策略是.deferredToDate,依赖系统底层的时间戳实现。如果selectedDate包含毫秒/微秒级精度,或者编码解码时策略不一致,会导致Set<Date>的存储和读取出现不匹配,甚至直接解码失败。try?隐藏了错误信息
代码中用try?处理编码解码,一旦抛出错误会直接返回nil,导致存储/读取失败却无法定位问题,表现为“有时能保存有时不能”的假象。不必要的
synchronize()调用
iOS 10+之后,UserDefaults会自动处理磁盘同步,手动调用synchronize()不仅无效,还可能引发线程安全问题,加剧数据存储的不可预测性。Date的Equatable依赖精确时间值
Set判断元素唯一性依赖Date的Equatable实现,而Date的相等性是基于精确到微秒的时间戳。如果同一个逻辑日期(比如同一天)的Date对象精度不同,会被Set视为不同元素,导致存储的集合不符合预期。
步骤1:统一Date的编码解码策略
修改UserDefaultsService,指定明确的Date编码策略(比如ISO8601),同时替换try?为do-catch打印错误,方便调试:
class UserDefaultsService { static let shared = UserDefaultsService() private let encoder: JSONEncoder = { let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 return encoder }() private let decoder: JSONDecoder = { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 return decoder }() func save<T: Codable>(_ value: T, forKey key: String) { do { let encoded = try encoder.encode(value) UserDefaults.standard.set(encoded, forKey: key) } catch { print("存储失败:\(error.localizedDescription)") } } func load<T: Codable>(forKey key: String, defaultValue: T) -> T { guard let data = UserDefaults.standard.data(forKey: key) else { return defaultValue } do { return try decoder.decode(T.self, from: data) } catch { print("读取失败:\(error.localizedDescription)") return defaultValue } } }
步骤2:统一Date的精度(关键解决方法)
由于Date的精确时间戳容易引发唯一性判断问题,建议将Date转换为固定精度的时间戳(比如当天0点的时间戳),再存储Set<TimeInterval>而非Set<Date>:
extension Date { // 获取当天0点的时间戳,仅保留日期精度 var startOfDayTimestamp: TimeInterval { let calendar = Calendar.current let startOfDay = calendar.startOfDay(for: self) return startOfDay.timeIntervalSince1970 } }
步骤3:修改DayInfoView的存储逻辑
将checkmarkVisible的类型改为Set<TimeInterval>,插入时使用转换后的时间戳:
struct DayInfoView: View { @State private var selectedDate: Date @Environment(\.presentationMode) var presentationMode private let userDefaultsService = UserDefaultsService.shared @State private var checkmarkVisible: Set<TimeInterval> = [] { didSet { userDefaultsService.save(checkmarkVisible, forKey: "checkmarkVisible") print(checkmarkVisible) } } init(selectedDate: Date) { _selectedDate = State(initialValue: selectedDate) let storedData: Set<TimeInterval> = userDefaultsService.load(forKey: "checkmarkVisible", defaultValue: []) _checkmarkVisible = State(initialValue: storedData) } var body: some View { // 原有界面代码... Button(action: { // 插入转换后的固定精度时间戳 checkmarkVisible.insert(selectedDate.startOfDayTimestamp) }) { Text("Button 2") .foregroundColor(.white) .padding() .background(Color.blue) .cornerRadius(8) } // 原有界面代码... } }
针对@AppStorage版本的适配
如果要继续使用@AppStorage,同样需要转换为Set<TimeInterval>,直接存储Data的话也需要遵循上面的编码策略:
struct DayInfoView: View { @State private var selectedDate: Date @Environment(\.presentationMode) var presentationMode @AppStorage("checkmarkVisible") private var checkmarkVisibleData: Data = Data() @State private var checkmarkVisible: Set<TimeInterval> = [] { didSet { let encoder = JSONEncoder() do { checkmarkVisibleData = try encoder.encode(checkmarkVisible) } catch { print("存储失败:\(error)") } print(checkmarkVisible) } } init(selectedDate: Date) { _selectedDate = State(initialValue: selectedDate) let decoder = JSONDecoder() if let stored = try? decoder.decode(Set<TimeInterval>.self, from: UserDefaults.standard.data(forKey: "checkmarkVisible") ?? Data()) { _checkmarkVisible = State(initialValue: stored) } } var body: some View { // 原有界面代码... Button(action: { checkmarkVisible.insert(selectedDate.startOfDayTimestamp) }) { Text("Button 2") .foregroundColor(.white) .padding() .background(Color.blue) .cornerRadius(8) } // 原有界面代码... } }
内容的提问来源于stack exchange,提问作者Vyacheslav

