How to create one-to-one relationships
How to create one-to-one relationships êŽë š
Updated for Xcode 15
One-to-one relationships mean that every X object has exactly one Y object attached to it, for example saying that every person has exactly one passport, or each pet has exactly one owner.
Although one-to-one relationships are possible with SwiftData, they are fairly rare, mostly because they are rare in real life â both of the two examples I just gave might seem reasonable at first, but if you think them through they fall apart pretty fast:
- Many people have more than one passport because they are citizens of more than one country, or have no passport because they donât travel.
- While itâs true that most pets have exactly one owner, a true one-to-one relationship means that each owner must have exactly one pet, which is clearly nonsense.
In practice, we mostly use one-to-one relationships in two places:
- Weâre splitting up our data to reduce redundancy or keep or code better organized, because writing
country.capitalCity.name
is simpler and easier than having one giantCountry
model that duplicates all the properties of a separateCity
model. - Weâre really trying to model an optional one-to-one relationship, i.e. zero-to-one: there might be exactly one piece of matching piece of data, but there also might be nothing at all. For example, a Mastodon user might have an
image
property set as their profile page header image, but itâs not required.
From a coding perspective, Swift requires that we declare one-to-one relationships as zero-to-one, even if they will never actually be zero-to-one. This is a simple fact of coding: if we try to make both properties non-optional, then we have a tortoise-and-hare problem where we canât create one without creating the other first.
For example, if we had Country
and City
models with a true one-to-one relationship between them, then to create a City
weâd need to specify which Country
it belongs to, but to create a Country
weâd need to specify its capitalCity
property.
So, to be able to create your objects and have SwiftData infer the relationship correctly, you should always make both sides of the relationship optional like this:
@Model
class Country {
var name: String
var capitalCity: City?
init(name: String, capitalCity: City? = nil) {
self.name = name
self.capitalCity = capitalCity
}
}
@Model
class City {
var name: String
var latitude: Double
var longitude: Double
var country: Country?
init(name: String, latitude: Double, longitude: Double, country: Country? = nil) {
self.name = name
self.latitude = latitude
self.longitude = longitude
self.country = country
}
}
With this approach our code becomes possible:
let country = Country(name: "England")
let city = City(name: "London", latitude: 51.507222, longitude: -0.1275, country: country)
modelContext.insert(city)
Important
Donât try to insert both the city
and country
objects â inserting one automatically inserts the other because the two have a relationship, and in fact trying to insert them both is likely to throw up a fatal error with the message, âDuplicate registration attempt for objectâ.