Skip to main content

Working with relationships

About 3 minSwiftArticle(s)bloghackingwithswift.comcrashcourseswiftswiftdataxcodeappstore

Working with relationships 관련

SwiftData by Example

Back to Home

Working with relationships | SwiftData by Example

Working with relationships

Updated for Xcode 15

So far we’ve had a simple data model containing a collection of destinations. To finish the app we’re going to upgrade this so that each destination has a list of sights that users want to visit there, for example they might say when they visit Rome they want to see the colosseum, the forum, the Vatican, and so on.

In SwiftData this is called a relationship: each destination has many sights to see. Rather than trying to cram all the sights into a single Destination object, we can instead create a separate Sight model, then tell SwiftData that our original Destination model has an array of sights – it will take care of linking the two for us.

To get started, create another new Swift file called Sight.swift, give it an import for SwiftData, then add this code there:

@Model
class Sight {
    var name: String

    init(name: String) {
        self.name = name
    }
}

That stores only a single piece of data, which is the name of the sight – later on you're welcome to add more to it, such as perhaps tracking whether the user has visited it already.

Now we can return to the Destination model and add a new property there:

var sights = [Sight]()

Just adding that property that is enough to tell SwiftData that each destination has many sights associated with it. Our original model didn't have that relationship, but that's okay: the next time you run the app, SwiftData will silently upgrade its data store to take this change into account with no extra work from us. This is called a migration, and it allows us to upgrade and adjust our models over time.

Of course, we need to actually add a way for users to list sights for each destination, and the simplest approach is to show a separate section in our view with a text field for new sight names.

First, add this new property to EditDestinationView:

@State private var newSightName = ""

That will track whatever the user is typing for their new sight name.

Second, add a new method that converts newSightName into an actual Sight object, then adds it to our destination's existing list of sights:

func addSight() {
    guard newSightName.isEmpty == false else { return }

    withAnimation {
        let sight = Sight(name: newSightName)
        destination.sights.append(sight)
        newSightName = ""
    }
}

And now we can add a new section to the form, looping over all the existing sights and also adding space to create new sights below:

Section("Sights") {
    ForEach(destination.sights) { sight in
        Text(sight.name)
    }

    HStack {
        TextField("Add a new sight in \(destination.name)", text: $newSightName)

        Button("Add", action: addSight)
    }
}

Notice how we can just access destination.sights directly? Relationships are loaded lazily by SwiftData, which means it will only load the sights for a destination only when they are actually used. This means DestinationListingView loads only the data it really needs, helping make sure our code remains fast and light on memory.

Now, before we're done with this relationship there's one tiny change I want to make. You see, right now we have a small problem: if our user decides they don't want to go to a destination they added, what should happen to all the sights they added to that destination?

SwiftData likes playing it safe by default, so in this situation deleting the destination will leave its sights intact and just hidden from view. Sometimes that's exactly what you'll want, but here it will just lead to clutter because there's no way of searching for sights that aren't attached to destinations.

This is when we need to give SwiftData a little extra guidance: we’re going to tell it that when we delete a destination it should also delete all the sights belonging to that destination.

To do that, we need to attach the @Relationship macro to the sights property, like this:

@Relationship(deleteRule: .cascade) var sights = [Sight]()

A cascade delete rule means "when we delete this object, delete all its sights too" – exactly what we want.


이찬희 (MarkiiimarK)
Never Stop Learning.