Inferred vs explicit relationships
Inferred vs explicit relationships êŽë š
Updated for Xcode 15
SwiftData relationships can be either inferred, where SwiftData can figure out the relationship just from your model structure, or made explicit using the @Relationship
macro. Generally speaking you only need @Relationship
when you want a non-default configuration; you can ignore it a lot of the time.
As an example, we could define a simple Student
model, then use that with a School
model:
@Model
class School {
var name: String
var students: [Student]
init(name: String, students: [Student]) {
self.name = name
self.students = students
}
}
@Model
class Student {
var name: String
var school: School
init(name: String, school: School) {
self.name = name
self.school = school
}
}
From that simple definition, SwiftData is able to see that:
- Each school can have many students.
- Each student must belong to precisely one school.
However, these two are separate things: if we create a student and set its school
property, SwiftData doesnât understand to add that student to the students
array in that school â it doesnât automatically infer that the relationship goes two ways.
However, if we make one small change to our Student
model, we do get a property relationship inference:
@Model
class Student {
var name: String
var school: School?
init(name: String, school: School?) {
self.name = name
self.school = school
}
}
The only change is that weâve marked the school as being optional, meaning that it can be nil. This happens for safety reasons, and to understand why consider this:
- If we have a relationship between students and schools, then setting the
school
property of a student should add it or remove it from a schoolâs list of students. - Equally, adding or removing a student from a schoolâs list of students should adjust the studentâs
school
property. - So what happens if you remove a student from a school without also adding them to another school?
When we defined the school
property as being non-optional weâre saying that latter case should be impossible â a student must always belong to a school. If we attempt to break this rule, SwiftData will trigger a crash in our app because weâve put it into an invalid state.
So, SwiftData takes the only safe approach by default: it will only infer the relationship when itâs safe to do so â when it wonât inadvertently trigger a crash because we changed an array. If you see an error along the lines of, âwarning: validation recovery attempt FAILED with Error Domain=NSCocoaErrorDomain Code=1570 %{PROPERTY}@ is a required valueâ then this is exactly what youâve hit: youâre trying to set a non-optional value to nil. Given that Swift refuses to let us do this directly, itâs probably happening through a relationship.
On the flip side, as soon as we made the school
property optional, that danger went away: removing a student from the students
array will just set their school
property to nil, so thereâs no crash risk.
The rule here is simple: if a relationship can be inferred safely, SwiftData will do so.
For the many times that isnât enough, we can create an explicit relationship using the @Relationship
macro on one of your two models, which spells out the connection explicitly. For example, we could change the Student
class so its school
property looks like this:
@Relationship(inverse: \School.students) var school: School
That can be optional or non-optional â there are no safety constraints here, because youâre telling SwiftData exactly what you want.
Alternatively, we can change the School
class so its students
property looks like this:
@Relationship(inverse: \Student.school) var students: [Student]
Important
Regardless of which option you choose, you should only specify the inverse relationship on one side. If you try specifying both, Xcode will throw up an error like âCircular reference resolving attached macro 'Relationshipâ.â
Using explicit relationships with the @Relationship
macro is also useful for specifying a custom delete rule to control how linked objects are deleted. For example, if you removed a school from your database, should SwiftData also remove all its students?
Important
Remember, this is an issue of safety: by using an explicit relationship youâre taking responsibility for ensuring you keep your models in a valid state.
We have four delete rules in SwiftData, with .nullify
being the default â set the related modelâs reference to nil when this object is deleted. If you have non-optional references â if you say that all students must have exactly one school, for example â then you should use a different delete rule instead.
For example, we might say that schools can have many students, and when we remove a student from our array we automatically delete that student object entirely. This uses the .cascade
delete rule, like so:
@Model
class School {
var name: String
@Relationship(deleteRule: .cascade, inverse: \Student.school) var students: [Student]
init(name: String, students: [Student]) {
self.name = name
self.students = students
}
}
If youâre still reading and arenât sure whether to use implicit or explicit relationships, Iâll give you a simple tip that has saved me so much time: Iâve never regretted using an explicit relationship, so I always prefer to spell out what I mean rather than relying on SwiftDataâs inference getting it right.