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.
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
}
}