Search code examples
jsonswiftcore-data

Saving Core Data to external backup file


I have a travel planner app I’m working on that uses Core Data for data storage. Two of my many entities (Person and Trip) have a many-to-many relationship (one person may go on many trips, and a single trip includes many people). I would like to add a feature that gives the user the opportunity to create an external backup file of their data as a txt file that can either live in iCloud, or a location in Files. I will need to be able to serialize and deserialize for backup and restore operations.

From what I’ve read it seems that JSON can only do one-to-many relationships (strictly hierarchical), not many-to-many. If I’m mistaken and it can indeed do many-to-many, can someone help me understand how to serialize that?

But if in fact it cannot, what other methods would be recommended for backing up Core Data into a txt file? Ideally this would be something that Swift has native methods for serializing and deserializing.


Solution

  • So, you have something like that:

    class Person: NSManagedObject {
        @NSManaged var id: UUID
        @NSManaged var trips: Set<Trip>
    }
    
    class Trip: NSManagedObject {
        @NSManaged var id: UUID
        @NSManaged var persons: Set<Person>
    }
    

    One way to share that (to a third party lib, a server, or in your case a copy), could be to transforms the "many-to-many" into arrays of UUIDs. That way, the other party just have to "redo" the connection if needed.

    If we think in JSON (but it could be XML, or any other format), one way would be, in "one big file mode"

    {
        "persons": [
                        { "id": "personId1", "trips": ["tripId1", 
                                                       "tripId2", 
                                                       "tripId3"]
                        },
                        { "id": "personId2", "trips": ["tripId3", 
                                                       "tripId5"]
                        },
                    ],
        "trips": [ 
                     {"id": "tripId1", "persons": ["personId1"]},
                     {"id": "tripId2", "persons": ["personId1"]},
                     {"id": "tripId3", "persons": ["personId1", "personId2"]},
                     {"id": "tripId4", "persons": []},
                     {"id": "tripId5", "persons": ["personId2"]},
                 ]
    }
    

    You can now create an export method:

    extension Person {
        func export() -> [String: Any] {
            ["id": id.uuidString,
             "trips": trips.map { $0.id.uuidString }]
        }
    }
    
    extension Trip {
        func export() -> [String: Any] {
            ["id": id.uuidString,
             "persons": persons.map { $0.id.uuidString }]
        }
    }
    

    You could also iterate on each properties to have something more "generic":

    extension Person {
        func exportAttributes() -> [String: Any] {
            //might need a `context.perform()` here, it depends who calls it, from where, could be async method, or a with a completion, it's up to your own code
            entity.attributesByName.reduce(into: [String: Any]()) { $0[$1.key] = value(forKey: $1.key) }
        }
        
            //Sample with computing some values
        func exportAttributesWithExtraWork() -> [String: Any] {
                    entity.attributesByName.reduce(into: [String: Any]()) {
                var finalKey = $1.key
                switch finalKey {
                case #keyPath(Person.id):
                    finalKey = "personId" //Here, we want to have another key
                default:
                    break
                }
                
                switch $1.value.attributeType {
                case .dateAttributeType:
                    guard let date = value(forKey: $1.key) as? Date else { return }
                    $0[finalKey] = date.timeIntervalSince1970 //It's just to show a sample where you change the value according to its type
                default:
                    $0[finalKey] = value(forKey: $1.key)
                }
            }
        }
        
        //Sample for relations
        func exportRelations() -> [String: Any] {
            entity.relationshipsByName.reduce(into: [String: Any]()) {
                if let trips = value(forKey: $1.key) as? Set<Trips> {
                    $0[$1.key] = trips.map { aTrip in aTrip.id.uuidString }
                }
            }
        }
    
        func fullExport() -> [String: Any] {
            var dictionary: [String: Any] = [:]
            let attributes = exportAttributes()
            dictionary.merge(attributes, uniquingKeysWith: { (_, new)  in return new })
            let relations = exportRelations()
            dictionary.merge(relations, uniquingKeysWith: { (_, new)  in return new })
            return dictionary 
        }
    }