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

Swift中带@DocumentID的Firestore模型存入UserDefaults失败问题

解决Firebase模型保存到UserDefaults的编解码问题

问题根源

  • 使用JSONEncoder时,@DocumentID是Firestore专属属性包装器,必须通过Firestore编码器处理,因此抛出编码不支持的错误。
  • 使用Firestore.Encoder时,编码器输出的结果包含FIRTimestamp这类非属性列表类型,而UserDefaults仅支持标准的Property List对象(如String、Number、Data、Array、Dictionary等),因此导致插入失败。

解决方案

方案一:拆分本地存储模型与Firestore模型

创建独立的本地存储模型,与Firestore模型解耦,转换后用标准JSON编解码处理:

// 本地存储专用模型
struct LocalDeviceModel: Codable, Hashable {
    var id: String?
    var fcmToken: String
    var userId: String
    var expiresOnTimestamp: TimeInterval? // 用时间戳替代Firestore Timestamp
    
    // 从Firestore模型转换
    init(from model: DeviceModel) {
        self.id = model.id
        self.fcmToken = model.fcmToken
        self.userId = model.userId
        self.expiresOnTimestamp = model.expiresOn?.date().timeIntervalSince1970
    }
    
    // 转换回Firestore模型
    func toFirestoreModel() -> DeviceModel {
        let expiresOn = expiresOnTimestamp.flatMap { Timestamp(date: Date(timeIntervalSince1970: $0)) }
        return DeviceModel(id: id, fcmToken: fcmToken, userId: userId, expiresOn: expiresOn)
    }
}

// 修改UserDefaults扩展,用标准JSON编解码
extension UserDefaults {
    func decode<T: Decodable>(_ type: T.Type, forKey defaultName: String) throws -> T {
        guard let data = data(forKey: defaultName) else {
            throw NSError(domain: "UserDefaults", code: -1, userInfo: [NSLocalizedDescriptionKey: "No data found for key \(defaultName)"])
        }
        return try JSONDecoder().decode(type, from: data)
    }
    
    func encode<T: Encodable>(_ value: T, forKey defaultName: String) throws {
        let data = try JSONEncoder().encode(value)
        set(data, forKey: defaultName)
    }
}

// 使用示例
let firestoreModel = DeviceModel(fcmToken: "token", userId: "user123")
let localModel = LocalDeviceModel(from: firestoreModel)
try UserDefaults.standard.encode(localModel, forKey: "localDeviceKey")

// 读取时转换回Firestore模型
if let localModel = try? UserDefaults.standard.decode(LocalDeviceModel.self, forKey: "localDeviceKey") {
    let firestoreModel = localModel.toFirestoreModel()
}

方案二:扩展Firestore类型支持JSON编解码

让Timestamp和@DocumentID支持标准JSON编解码,直接复用原模型:

// 扩展Timestamp支持JSON编解码
extension Timestamp: Codable {
    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode([
            "seconds": seconds,
            "nanoseconds": nanoseconds
        ])
    }
    
    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let dict = try container.decode([String: Int64].self)
        self.init(seconds: dict["seconds"] ?? 0, nanoseconds: dict["nanoseconds"] ?? 0)
    }
}

// 手动处理@DocumentID的编解码(属性包装器默认只支持Firestore编码器)
extension DeviceModel {
    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(id, forKey: .id)
        try container.encode(fcmToken, forKey: .fcmToken)
        try container.encode(userId, forKey: .userId)
        try container.encode(expiresOn, forKey: .expiresOn)
    }
    
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decodeIfPresent(String.self, forKey: .id)
        fcmToken = try container.decode(String.self, forKey: .fcmToken)
        userId = try container.decode(String.self, forKey: .userId)
        expiresOn = try container.decodeIfPresent(Timestamp.self, forKey: .expiresOn)
    }
}

// 使用标准JSONEncoder/Decoder的UserDefaults扩展(同方案一)
// 直接编码原DeviceModel
let model = DeviceModel(fcmToken: "token", userId: "user123")
try UserDefaults.standard.encode(model, forKey: "localDeviceKey")

方案三:用归档方式存储Firestore编码器输出

将Firestore.Encoder的结果通过NSKeyedArchiver转成Data,再存入UserDefaults:

// 修改UserDefaults扩展
extension UserDefaults {
    func decode<T: Decodable>(_ type: T.Type, forKey defaultName: String) throws -> T {
        guard let data = data(forKey: defaultName) else {
            throw NSError(domain: "UserDefaults", code: -1, userInfo: [NSLocalizedDescriptionKey: "No data found for key \(defaultName)"])
        }
        guard let object = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) else {
            throw NSError(domain: "UserDefaults", code: -2, userInfo: [NSLocalizedDescriptionKey: "Failed to unarchive data"])
        }
        return try Firestore.Decoder().decode(type, from: object as! [String: Any])
    }
    
    func encode<T: Encodable>(_ value: T, forKey defaultName: String) throws {
        let firestoreData = try Firestore.Encoder().encode(value)
        let data = try NSKeyedArchiver.archivedData(withRootObject: firestoreData, requiringSecureCoding: false)
        set(data, forKey: defaultName)
    }
}

// 使用示例
let model = DeviceModel(fcmToken: "token", userId: "user123")
try UserDefaults.standard.encode(model, forKey: "localDeviceKey")

let decodedModel = try UserDefaults.standard.decode(DeviceModel.self, forKey: "localDeviceKey")

内容的提问来源于stack exchange,提问作者ALex

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 06:45:39