SwiftData allows us to create models that reference each other, for example saying that a School
model has an array of many Student
objects, or an Employee
model stores a Manager
object.
These are called relationships, and they come in all sorts of forms. SwiftData does a good job of forming these relationships automatically as long as you tell it what you want, although there's still some room for surprises!
Let's try them out now. We already have the following User
model:
@Model
class User {
var name: String
var city: String
var joinDate: Date
init(name: String, city: String, joinDate: Date) {
self.name = name
self.city = city
self.joinDate = joinDate
}
}
We could extend that to say that each User
can have an array of jobs attached to them – tasks they need to complete as part of their work. To do that, we first need to create a new Job
model, like this:
@Model
class Job {
var name: String
var priority: Int
var owner: User?
init(name: String, priority: Int, owner: User? = nil) {
self.name = name
self.priority = priority
self.owner = owner
}
}
Notice how I've made the owner
property refer directly to the User
model – I've told SwiftData explicitly that the two models are linked together.
And now we can adjust the User
model to create the jobs array:
var jobs = [Job]()
So, jobs have an owner, and users have an array of jobs – the relationship goes both ways, which is usually a good idea because it makes your data easier to work with.
That array will start working immediately: SwiftData will load all the jobs for a user when they are first requested, so if they are never used at all it will just skip that work.
Even better, the next time our app launches SwiftData will silently add the jobs
property to all its existing users, giving them an empty array by default. This is called a migration: when we add or delete properties in our models, as our needs evolve over time. SwiftData can do simple migrations like this one automatically, but as you progress further you'll learn how you can create custom migrations to handle bigger model changes.
Tip: When we used the modelContainer()
modifier in our App
struct, we passed in User.self
so that SwiftData knew to set up storage for that model. We don't need to add Job.self
there because SwiftData can see there's a relationship between the two, so it takes care of both automatically.
You don't need to change the @Query
you use to load your data, just go ahead and use the array like normal. For example, we could show a list of users and their job count like this:
List(users) { user in
HStack {
Text(user.name)
Spacer()
Text(String(user.jobs.count))
.fontWeight(.black)
.padding(.horizontal, 10)
.padding(.vertical, 5)
.background(.blue)
.foregroundStyle(.white)
.clipShape(.capsule)
}
}
If you want to see it work with some actual data, you can either create a SwiftUI view to create new Job
instances for the selected user, but for testing purposes we can take a little shortcut and add some sample data.
First add a property to access the active SwiftData model context:
@Environment(\.modelContext) var modelContext
And now add a method such as this one, to create some sample data:
func addSample() {
let user1 = User(name: "Piper Chapman", city: "New York", joinDate: .now)
let job1 = Job(name: "Organize sock drawer", priority: 3)
let job2 = Job(name: "Make plans with Alex", priority: 4)
modelContext.insert(user1)
user1.jobs.append(job1)
user1.jobs.append(job2)
}
Again, notice how almost all that code is just regular Swift – only one line actually relates to SwiftData.
I encourage you to try experimenting here a little bit. Your starting point should always be to assume that working with your data is just like working with a regular @Observable
class – just let SwiftData do its thing until you have a reason to do otherwise!
There is one small catch, though, and it's worth covering before we move on: we've linked User
and Job
so that one user can have lots of jobs to do, but what happens if we delete a user?
The answer is that all their jobs remain intact – they don't get deleted. This is a smart move from SwiftData, because you don't get any surprise data loss.
If you specifically want all a user's job objects to be deleted at the same time, we need to tell SwiftData that. This is done using an @Relationship
macro, providing it with a delete rule that describes how Job
objects should be handled when their owning User
is deleted.
The default delete rule is called .nullify
, which means the owner
property of each Job
object gets set to nil, marking that they have no owner. We're going to change that to be .cascade
, which means deleting a User
should automatically delete all their Job
objects. It's called cascade because the delete keeps going for all related objects – if our Job
object had a locations
relationship, for example, then those would also be deleted, and so on.
So, change the jobs
property in User
to this:
@Relationship(deleteRule: .cascade) var jobs = [Job]()
And now we're being explicit, which means we don't leave any hidden Job
objects around when deleting a user – much better!
SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure and A/B test your entire paywall UI without any code changes or app updates.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.