新手求教:如何将Firebase Realtime数组数据同步到CoreData?
问题描述
我是CoreData新手,不清楚如何将Firebase Realtime数据库中的数组数据写入和读取到CoreData中。目前在处理Place实体的数组数据时存在困惑,希望了解正确的处理方法。
Firebase数据结构

CoreData实体定义


FirebaseController代码
final class FirebaseController: ObservableObject { @Published var birds = [Bird]() @Published var places = [Place]() private lazy var databaseDB: DatabaseReference? = { let reference = Database.database().reference().child("db") return reference }() private lazy var birdsDB: DatabaseReference? = { let reference = Database.database().reference().child("db").child("birds") return reference }() private lazy var placesDB: DatabaseReference? = { let reference = Database.database().reference().child("db").child("places") return reference }() private let context = PersistenceController.shared.viewContext init() { listentoRealtimeDatabase() } func listentoRealtimeDatabase() { guard let dbBirds = birdsDB else { return } guard let dbPlaces = placesDB else { return } dbBirds .observe(.childAdded) { [weak self] snapshot, context in guard let self = self, var json = snapshot.value as? [String: Any] else { return } json["id"] = snapshot.key do { let birdData = try JSONSerialization.data(withJSONObject: json) let decoder = JSONDecoder(context: self.context) let bird = try decoder.decode(Bird.self, from: birdData) print("success \(String(describing: bird.name!)) \(String(describing: bird.imageURL!))") if !self.birds.contains(where: { $0.name == bird.name }) { self.birds.append(bird) self.save() } print("Count: \(self.birds.count)") } catch { print("Bird解码失败:\(error)") } } dbPlaces .observe(.childAdded) { [weak self] snapshot, context in guard let self = self, var json = snapshot.value as? [String: Any] else { return } json["id"] = snapshot.key do { let placeData = try JSONSerialization.data(withJSONObject: json) let decoder = JSONDecoder(context: self.context) let place = try decoder.decode(Place.self, from: placeData) print("success \(String(describing: place.id!))") if !self.places.contains(where: { $0.id == place.id }) { self.places.append(place) print(place.placeNames) self.save() } print("Count: \(self.places.count)") } catch { print("Place解码失败:\(error)") } } // 获取CoreData中的数据 getBirdsFromCoreData() getPlacesFromCoreData() } func getBirdsFromCoreData() { let request = Bird.fetchRequest() request.returnsObjectsAsFaults = false if let results = try? self.context.fetch(request) { self.birds = results } } func getPlacesFromCoreData() { let request = Place.fetchRequest() request.returnsObjectsAsFaults = false if let results = try? self.context.fetch(request) { self.places = results } print(places.count) } func stopListening() { databaseDB!.removeAllObservers() } func deleteRecord(indexSet: IndexSet) { let itemToDelete = birds[indexSet.first!] context.delete(itemToDelete) save() getBirdsFromCoreData() getPlacesFromCoreData() } private func save() { guard context.hasChanges else { return } do { try context.save() print("Saved changes.") } catch { print("Could not save changes in context: \(error)") } } }
CoreData实体类代码
@objc(Bird) public class Bird: NSManagedObject, Identifiable, Codable { enum CodingKeys: String, CodingKey { case imageURL case name } static let managedObjectContext = CodingUserInfoKey(rawValue: "managedObjectContext") @nonobjc public class func fetchRequest() -> NSFetchRequest<Bird> { return NSFetchRequest<Bird>(entityName: "Bird") } @NSManaged public var id: String? @NSManaged public var name: String? @NSManaged public var imageURL: String? @NSManaged public var places: NSSet? public var birdPlases: [Place] { let placesSet = self.places as? Set<Place> ?? [] return placesSet.sorted { $0.placeNames.first ?? "" > $1.placeNames.first ?? "" } } public var birdName: String { return name ?? "" } public var image: String { return imageURL ?? "" } public required convenience init(from decoder: Decoder) throws { guard let context = decoder.userInfo[.context] as? NSManagedObjectContext else { fatalError("缺少Managed Object Context") } let entity = NSEntityDescription.entity(forEntityName: "Bird", in: context)! self.init(entity: entity, insertInto: context) let values = try decoder.container(keyedBy: CodingKeys.self) name = try values.decode(String.self, forKey: .name) imageURL = try values.decode(String.self, forKey: .imageURL) } // MARK: - Encodable public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(name, forKey: .name) try container.encode(imageURL, forKey: .imageURL) } } extension CodingUserInfoKey { static let context = CodingUserInfoKey(rawValue: "context")! } extension JSONDecoder { convenience init(context: NSManagedObjectContext) { self.init() self.userInfo[.context] = context } } extension Bird { @objc(addPlaceObject:) @NSManaged public func addToPlaces(_ value: Place) @objc(removePlaceObject:) @NSManaged public func removeFromPlaces(_ value: Place) @objc(addPlaces:) @NSManaged public func addToPlaces(_ value: NSSet) @objc(removePlaces:) @NSManaged public func removeFromPlaces(_ value: NSSet) } @objc(Place) public class Place: NSManagedObject, Identifiable, Codable { enum CodingKeys: String, CodingKey { case names } @nonobjc public class func fetchRequest() -> NSFetchRequest<Place> { return NSFetchRequest<Place>(entityName: "Place") } @NSManaged public var id: String? @NSManaged public var names: [String]? public var placeNames: [String] { return names ?? [] } public required convenience init(from decoder: Decoder) throws { guard let context = decoder.userInfo[.context] as? NSManagedObjectContext else { fatalError("缺少Managed Object Context") } let entity = NSEntityDescription.entity(forEntityName: "Place", in: context)! self.init(entity: entity, insertInto: context) let values = try decoder.container(keyedBy: CodingKeys.self) names = try values.decodeIfPresent([String].self, forKey: .names) } // MARK: - Encodable public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(names, forKey: .names) } }
解决方案
一、核心问题修正:Place实体的数组处理
从Firebase数据结构看,places节点下的names是字符串数组,但你之前将CoreData的names属性定义为Data类型,导致解码失败。推荐直接用Transformable类型存储数组:
修改CoreData模型:
- 打开CoreData模型编辑器,将Place实体的
names属性类型改为Transformable - 设置
Transformer为NSSecureUnarchiveFromDataTransformer(iOS 14+),勾选Allow External Storage - 将Place的
id属性类型从UUID改为String,匹配Firebase返回的字符串ID
- 打开CoreData模型编辑器,将Place实体的
更新Place类代码:
直接将names定义为[String]?类型,CoreData会自动处理序列化/反序列化,上面的代码已经完成了这部分修改。
二、FirebaseController代码修复
- 修复闭包语法错误:原代码中
dbPlaces.observe的闭包多了一层嵌套,已修正 - 显式调用
self.save():在闭包中必须通过self访问类方法,否则会报错 - 优化重复判断逻辑:用
id判断重复比name更可靠,避免名称重复导致的数据丢失
三、多对多关联处理(可选)
如果Firebase的birds节点包含关联的Place ID数组,需要在解码Bird时维护关联关系:
// 在Bird的init(from decoder:)中添加以下代码 if let placeIds = try values.decodeIfPresent([String].self, forKey: .placeIds) { for placeId in placeIds { let request = Place.fetchRequest() request.predicate = NSPredicate(format: "id == %@", placeId) if let place = try? context.fetch(request).first { addToPlaces(place) } } }
四、调试技巧
- 解码失败时打印完整错误信息,比如
print("Place解码失败:\(error.localizedDescription),\(error)"),定位具体问题 - 检查Firebase返回的JSON键名是否和
CodingKeys完全匹配 - 使用Xcode的Persistent Store Debugger查看CoreData中的数据存储情况
内容的提问来源于Stack Exchange,提问作者Max
相关产品推荐
相关产品推荐

