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
相关产品推荐
相关产品推荐

