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

SwiftUI中@AppStorage与UserDefaults在init/didSet中失效的原因与解决

问题原因分析
  1. Date类型的Codable编码解码不稳定
    JSONEncoder/Decoder默认对Date的编码策略是.deferredToDate,依赖系统底层的时间戳实现。如果selectedDate包含毫秒/微秒级精度,或者编码解码时策略不一致,会导致Set<Date>的存储和读取出现不匹配,甚至直接解码失败。

  2. try?隐藏了错误信息
    代码中用try?处理编码解码,一旦抛出错误会直接返回nil,导致存储/读取失败却无法定位问题,表现为“有时能保存有时不能”的假象。

  3. 不必要的synchronize()调用
    iOS 10+之后,UserDefaults会自动处理磁盘同步,手动调用synchronize()不仅无效,还可能引发线程安全问题,加剧数据存储的不可预测性。

  4. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 09:22:07