Part of my app involves a genogram (basically a family tree of genetic relations). That means I really only have 1 model type – a Relative, who has relation types to the person at the core of the tree (the “index” person). I’ve settled on a model of having each person defining their nuclear family (parents, siblings, children, and spouse(s)), in addition to their relation to the index. This would allow the tree to be drawn (when I get that far) by traversing the relationships, and gives an easy way for the user to verify things. It seems necessary to do this way because I don’t just need to know Person B is the aunt of Index, I need to know how – Person B is sister of Person A who is father to index, etc.
Issues
I can’t properly define inverse relationships, because there’s only 1 model – inverse of siblings is siblings, which creates a circular reference. Same for spouses. Inverse of both mom and dad is children – but if you tell SwiftData that, then when you add Parent A is mom of Index, Parent A also gets added as dad.
I left off the relationships and tried to add everything in my savePerson method, and have it add the relationship, the inverse, and other things it can already guess. For example, if you add mom first, then when you add dad to index person, it will automatically put mom and dad as spouses. Then add a sibling to index, and it will also make that person child of mom and dad.
This all looks like it works! Run the app, add a bunch of people, all relations are added as they should be, with the program making assumptions/guesses where appropriate.
But after closing the app and reopening, some relationships are missing.
Examples:
- Add Index, then Mom -> Mom and Index are properly inverses of each other and persist on opening/closing; then add Dad and check Mom/Dad are spouses and Index is child of both -> however, on reopening, Mom suddenly has no children
- same happens in opposite direction – add Dad first and then Mom and everything works, but reopen and Dad will lose Index
- Add Mom, Dad, Sister, then Dad’s brother and sister as Aunt and Uncle -> again, you can verify that all relationships work correctly at first -> on reopening, Dad has no siblings, one of Aunt or Uncle has one, the other has (correctly) 2
Forcing modelContext.save() after the savePerson method didn’t help, or surface the bug before reopening. I assume something is happening in SwiftData “correcting” what it thinks the relationships should be – if it were an error in my code itself, I assume I’d see a problem immediately, not on restart? I don’t manipulate the save data on close or restart of the app (at least not deliberately).
Of note, I also tried to re-jig the savePerson method a few ways to do things in a different order, add relationships from only one side (see if the other side would be assumed by SwiftData), etc. – all attempts either caused crashes (I think for invalid inverse relationships) or just led to the same error where things get dropped on restart. Changing where I call modelContext.insert also didn’t matter.
So if SwiftData’s assumptions are causing the issue, is there a proper way to define the necessary relationships when I have only this 1 model? Or stop SwiftData from doing its thing so I can control all relationships manually?
Model and enums
@Model
class Relative {
let name: String
let gender: Gender
let dob: Date
let isIndex: Bool
let relationToIndex: RelationType
var qualifier: RelationQualifier?
var diagnoses: [String] = []
var mother: Relative? = nil
var father: Relative? = nil
var spouses: [Relative] = []
var siblings: [Relative] = []
var children: [Relative] = []
init(name: String, gender: Gender, dob: Date, isIndex: Bool = false, relationToIndex: RelationType, qualifier: RelationQualifier? = nil, diagnoses: [String] = [], mother: Relative? = nil, father: Relative? = nil, spouses: [Relative] = [], siblings: [Relative] = [], children: [Relative] = []) {
self.name = name
self.gender = gender
self.dob = dob
self.isIndex = isIndex
self.relationToIndex = relationToIndex
self.qualifier = qualifier
self.diagnoses = diagnoses
self.mother = mother
self.father = father
self.spouses = spouses
self.siblings = siblings
self.children = children
}
}
enum Gender: String, CaseIterable, Identifiable, Codable {
case male = "Male"
case female = "Female"
var id: Self { self }
}
enum RelationType: String, CaseIterable, Identifiable, Codable {
case undefined = "N/A"
case index = "Index"
case mother = "Mother"
case father = "Father"
case brother = "Brother"
case sister = "Sister"
case brotherInLaw = "Brother-in-law"
case sisterInLaw = "Sister-in-law"
case son = "Son"
case daughter = "Daughter"
case sonInLaw = "Son-in-law"
case daughterInLaw = "Daughter-in-law"
case grandson = "Grandson"
case granddaughter = "Granddaughter"
case husband = "Husband"
case wife = "Wife"
case aunt = "Aunt"
case uncle = "Uncle"
case cousin = "Cousin"
case grandmother = "Grandmother"
case grandfather = "Grandfather"
case niece = "Niece"
case nephew = "Nephew"
var id: Self { self }
}
enum PrimaryRelationType: String, CaseIterable, Identifiable, Codable {
case parent = "Parent"
case sibling = "Sibling"
case spouse = "Spouse"
case child = "Child"
var id: Self { self }
}
enum RelationQualifier: String, CaseIterable, Identifiable, Codable {
case none = "None"
case grand = "Grand"
case great = "Great"
case step = "Step"
case half = "Half"
case first = "First"
case second = "Second"
case third = "Third"
case fourth = "Fourth"
var id: Self { self }
}
Primary method in question
func savePerson() {
let components = Calendar.current.dateComponents([.year, .month, .day], from: dob)
let newDate = Date.from(year: components.year!, month: components.month!, day: components.day!)
let newRelative = Relative(name: name, gender: gender, dob: newDate, relationToIndex: relation, diagnoses: diagnoses)
let relativeToAddTo: Relative
if self.relativeToAddTo != nil {
relativeToAddTo = self.relativeToAddTo!
} else {
relativeToAddTo = self.index
}
if relativeToAdd == .child {
if relativeToAddTo.gender == .female {
newRelative.mother = relativeToAddTo
//copy father from the last spouse of origin relative
if let father = relativeToAddTo.spouses.last {
newRelative.father = father
}
} else {
newRelative.father = relativeToAddTo
if let mother = relativeToAddTo.spouses.last {
newRelative.mother = mother
}
}
//origin parent's kids are new entry's siblings
newRelative.siblings += relativeToAddTo.children
//add reciprocal children/siblings
for child in relativeToAddTo.children {
child.siblings.append(newRelative)
}
//add this child now to the parent
relativeToAddTo.children.append(newRelative)
} else if relativeToAdd == .parent {
newRelative.children.append(relativeToAddTo)
if gender == .female {
relativeToAddTo.mother = newRelative
//add origin relative's father; we shouldn't need to check if there already is one, because this relative is new, so [spouses] is empty
if let father = relativeToAddTo.father {
newRelative.spouses.append(father)
//if the father exists, this new relative is a spouse
father.spouses.append(newRelative)
}
} else if gender == .male {
relativeToAddTo.father = newRelative
//add mother, if she exists
if let mother = relativeToAddTo.mother {
newRelative.spouses.append(mother)
//and reciprocate
mother.spouses.append(newRelative)
}
}
//origin child's siblings are the new relative's children
newRelative.children += relativeToAddTo.siblings
} else if relativeToAdd == .sibling {
//copy any siblings that already exist
newRelative.siblings += relativeToAddTo.siblings
//also add this person to those siblings' lists
for sibling in relativeToAddTo.siblings {
sibling.siblings.append(newRelative)
}
//then, add the new relative as a sibling, and reciprocal
newRelative.siblings.append(relativeToAddTo)
relativeToAddTo.siblings.append(newRelative)
//siblings can be assumed to share parents
if let mother = relativeToAddTo.mother {
newRelative.mother = mother
}
if let father = relativeToAddTo.father {
newRelative.father = father
}
} else if relativeToAdd == .spouse {
newRelative.spouses.append(relativeToAddTo)
relativeToAddTo.spouses.append(newRelative)
//nothing further to do
//spouses can't be assumed to share kids
//we don't know which marriage each child came from
//user must manually fix this
}
modelContext.insert(newRelative)
do {
try modelContext.save()
} catch(let error) {
print("error: (error.localizedDescription)")
}
dismiss()
}