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

新手求教:如何将Firebase Realtime数组数据同步到CoreData?

问题描述

我是CoreData新手,不清楚如何将Firebase Realtime数据库中的数组数据写入和读取到CoreData中。目前在处理Place实体的数组数据时存在困惑,希望了解正确的处理方法。

Firebase数据结构

Firebase数据结构

CoreData实体定义

Bird实体
Place实体

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类型存储数组:

  1. 修改CoreData模型:

    • 打开CoreData模型编辑器,将Place实体的names属性类型改为Transformable
    • 设置Transformer为NSSecureUnarchiveFromDataTransformer(iOS 14+),勾选Allow External Storage
    • 将Place的id属性类型从UUID改为String,匹配Firebase返回的字符串ID
  2. 更新Place类代码:
    直接将names定义为[String]?类型,CoreData会自动处理序列化/反序列化,上面的代码已经完成了这部分修改。

二、FirebaseController代码修复

  1. 修复闭包语法错误:原代码中dbPlaces.observe的闭包多了一层嵌套,已修正
  2. 显式调用self.save():在闭包中必须通过self访问类方法,否则会报错
  3. 优化重复判断逻辑:用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)
        }
    }
}

四、调试技巧

  1. 解码失败时打印完整错误信息,比如print("Place解码失败:\(error.localizedDescription),\(error)"),定位具体问题
  2. 检查Firebase返回的JSON键名是否和CodingKeys完全匹配
  3. 使用Xcode的Persistent Store Debugger查看CoreData中的数据存储情况

内容的提问来源于Stack Exchange,提问作者Max

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 16:47:02