How to create many-to-many relationships
How to create many-to-many relationships êŽë š
Updated for Xcode 15
Many-to-many relationships in SwiftData are created when both sides of a relationship use arrays. They are fairly common, too: some actors appear in many films and most films have many actors, some authors write many books and some books have several authors, and so on.
Important
SwiftData will not infer many-to-many relationships; you must make them explicit using the @Relationship
macro. Many-to-many relationships are easy to get wrong, so please read the below carefully.
First, declare your models, using @Relationship
in one of the two so you make clear the inverse of your relationship. For example, we might have say actors can star in many films, and films can have many actors:
@Model
class Actor {
var name: String
var movies: [Movie]
init(name: String, movies: [Movie]) {
self.name = name
self.movies = movies
}
}
@Model
class Movie {
var name: String
var releaseYear: Int
@Relationship(inverse: \Actor.movies) var cast: [Actor]
init(name: String, releaseYear: Int, cast: [Actor]) {
self.name = name
self.releaseYear = releaseYear
self.cast = cast
}
}
Now create and link your data, like so:
let mi2 = Movie(name: "Mission: Impossible 2", releaseYear: 2000, cast: [])
let cruise = Actor(name: "Tom Cruise", movies: [mi2])
let newton = Actor(name: "Thandiwe Newton", movies: [mi2])
modelContext.insert(cruise)
modelContext.insert(newton)
Note
We donât need to insert the Movie
object â it will be inserted automatically by SwiftData because itâs used by the two actors.
There are lots of ways you can get many-to-many relationships wrong, and Iâm afraid SwiftData is entirely unforgiving here â if you donât follow the steps correctly things will just behave strangely, or even crash at runtime.
For example, if you attempt to manipulate the movies
property of an actor before inserting them, youâll get a hard crash. So, this code wonât work:
let mi2 = Movie(name: "Mission: Impossible 2", releaseYear: 2000, cast: [])
let cruise = Actor(name: "Tom Cruise", movies: [])
let newton = Actor(name: "Thandiwe Newton", movies: [])
mi2.cast.append(cruise)
mi2.cast.append(newton)
modelContext.insert(mi2)
modelContext.insert(cruise)
modelContext.insert(newton)
Youâll also get a crash with the message âillegal attempt to establish a relationshipâ if you attempt to insert things out of sequence, like this:
let mi2 = Movie(name: "Mission: Impossible 2", releaseYear: 2000, cast: [])
modelContext.insert(mi2)
let cruise = Actor(name: "Tom Cruise", movies: [mi2])
let newton = Actor(name: "Thandiwe Newton", movies: [mi2])
modelContext.insert(cruise)
modelContext.insert(newton)
And just for fun, if you accidentally miss off the @Relationship
macro what youâll get is pretty much random even when you insert things in the correct order.
If you need to manipulate the arrays directly, itâs important that all your data be inserted first. For our movies and actors example, that would mean creating all the data, inserting it, then manipulating the arrays, like this:
let mi2 = Movie(name: "Mission: Impossible 2", releaseYear: 2000, cast: [])
let cruise = Actor(name: "Tom Cruise", movies: [])
let newton = Actor(name: "Thandiwe Newton", movies: [])
modelContext.insert(mi2)
modelContext.insert(cruise)
modelContext.insert(newton)
mi2.cast.append(cruise)
mi2.cast.append(newton)
Or we could add the movies to the actors:
cruise.movies.append(mi2)
newton.movies.append(mi2)
Important
There is a rather catastrophic SwiftData bug in iOS 17.0 where many-to-many relationships will fail based on the alphabetical ordering of your model names. This means in the above examples models named Actor
and Movie
will work but Movie
and Person
will not. Until a fix is released, a workaround is to provide a default value for your relationship array, like this: var movies: [Movie] = []
.