UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Relationships with SwiftData, SwiftUI, and @Query

Paul Hudson    @twostraws   

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!

TAKE YOUR SKILLS TO THE NEXT LEVEL If you like Hacking with Swift, you'll love Hacking with Swift+ – it's my premium service where you can learn advanced Swift and SwiftUI, functional programming, algorithms, and more. Plus it comes with stacks of benefits, including monthly live streams, downloadable projects, a 20% discount on all books, and free gifts!

Find out more

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.3/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.