SwiftData and 'Circular references'

Using the 'Create SwiftData Code...' action in Xcode results in Model that have bi-directional relationships between many of my entities (which is correct - my Core Data model includes this).

All of these Relationships are marked as invalid, however, with the following error:

Circular reference resolving attached macro 'Relationship'

Most ORM's, in my experience, have a way to mark one end of a bidirectional relationship as an 'owner'. Is there something similar available in SwiftData, or is this scenario handled in a different way? Or am I going to need to remove all of my bidirectional relationships?

Post not yet marked as solved Up vote post of Mcorey Down vote post of Mcorey
4.1k views

Replies

Ok, after reviewing the 'Trips' sample application that accompanied the 'Dive deeper into SwiftData' video from WWDC '23, I see that this is handled between the Trip and both the BucketListItem and LivingAccommodation entities by simply omitting the @Relationship annotation on the 'child' entities. I have a few questions, based on what I see:

  • If a LivingAccommodation entity is loaded via a @Query, will the trips field be populated by the SwiftData system?
  • On both child entities, the trip field is an Optional Trip - is this required in this scenario? I would imagine that it doesn't make sense to have a LivingAccommodation object without an associated Trip object, and my own Model is similar

I'm honestly lost on the automatic inverse relationships. I've reviewed the SampleTrips project and followed what they do there and it doesn't work.

From what I can see, you don't need to do anything other than mark the inverse and then:

let item = BucketListItem(
            title: "A bucket list item title",
            details: "Details of my bucket list item",
            hasReservation: true, isInPlan: true)
        item.trip = .preview

but when I review the code of the project in the section where they add bucket list items, they do set the inverse?

private func addItem() {
        withAnimation {
            let newItem = BucketListItem(title: title, details: details, hasReservation: hasReservations, isInPlan: isInPlan)
            modelContext.insert(newItem)
            newItem.trip = trip
            trip.bucketList.append(newItem)
        }
    }

this doesn't seem to make sense to me?

First I was confused as well, but your own comment @Mcorey and looking at the example code helped me to figure it out.
As you’ve said you basically have to omit the @Relationship in one of the entities and replace it with a simple variable in the other one.

Example Code

Main Entity:

@Model
final public class MainItem {
    var title: String

    @Relationship(inverse: \ChildItem.item)
    var childs: [ChildItem]?
}

Child Entity:

@Model
final public class ChildItem {
    var timestamp: Date

    // Add inverse as a simple variable
    var item: ChildItem?
}

I ran into this today and this did work for me, however I also saw https://developer.apple.com/forums/thread/731921 where it seems simply deleting the "inverse" parameter out of the relationship also works.

I haven't tried yet, but perhaps this will solve the automatic inverse relationships things?

Example Code

Main Entity:

@Model
final public class MainItem {
    var title: String

    @Relationship(inverse: \ChildItem.item)
    var childs: [ChildItem]?
}

Child Entity:

@Model
final public class ChildItem {
    var timestamp: Date

    // Add inverse as a simple variable
    @Relationship var item: MainItem?
}

Don't forget to use .cascade if you want to delete the related models when you delete the parent model.

Well, my code is crashing with the following error.

SwiftData/BackingData.swift:367: Fatal error: Unknown related type - ChildItem

My MainItem @Relationship

@Relationship(deleteRule: .cascade, originalName: "events", inverse: \ChildItem.item) var items: [ChildItem]?

My ChildItem

var item: MainItem?

It also crashes when using @Relationship var item: MainItem? for the ChildItem.

  • Well, it crashed because I initialized items to nil, init(items: [ChildItem]? = nil). But init (items: [ChildItem]? = []) worked, did not crash.

Add a Comment

Actually, I think it can be as simple as the following example

@Model
final public class MainItem {
    var title: String?

    @Relationship(deleteRule: .cascade)
    var childs: [ChildItem]?

    init(title: String? = nil, childs: [ChildItem]? = []) {
        self.title = title
        self.childs = childs
    }
}

And the child item is.

@Model
final public class ChildItem {
    var timestamp: Date?

    // Add inverse as a simple variable
    var item: MainItem?

    init(timestamp: Date? = nil, item: MainItem? = nil) {
        self.timestamp = timestamp
        self.item = item
    }
}

In the init of the MainItem, set the childs to 'childs: [ChildItem]? = [].' Do NOT set it to nil or you'll crash. I did.

  • Setting nil to an optional should be obvious, but yeah also got it only to work with []

Add a Comment

I have been struggling with this for weeks. I am about to try your code @SpaceMan , but I am confused why the ChildItem has the item property as an optional. Surely every ChildItem should have a MainItem? Also, I think that I understand 'final', but why are your Models public?

@Bruckner I let Xcode generate the models from Core Data models. That is what resulted.