@SectionedFetchRequest using relationship as sectionIdentifier

As the title says I'm trying to use a to-one relationship as a sectionIdentifier in a @SectionedFetchRequest. The compiler is happy but there's a runtime crash:

Could not cast value of type '_NSCoreDataTaggedObjectID' (0x146c0f750) to 'MyApp.ServiceCategory' (0x104c4b3a0).

The fetch request:

@SectionedFetchRequest(
    sectionIdentifier: \Service.serviceCategory,
    sortDescriptors: [
      SortDescriptor(\Service.active, order: .reverse),
      SortDescriptor(\Service.displayText)
    ],
    predicate: NSPredicate(format: "%K = %d", #keyPath(Service.active), true),
    animation: .default
  ) var sectionedServices: SectionedFetchResults<ServiceCategory?, Service>

... and the breaking runtime code:

ForEach(sectionedServices /* here */) { section in
  Section(header: Text(section.id?.displayText ?? "")) {
    ForEach(section) { svc in
      Text(svc.displayText ?? "")
    }
  }
}

The request works if I switch out the sectionIdentifier for the active property (which is a Bool property rather than a relationship). It also works if I switch it out for displayText which is an optional String, so it seems to be a problem trying to section by a relationship rather than with it being an optional.

The error suggests the request is returning a Core Data fault rather than an object but my attempts to somehow unwrap this haven't gone very far.

Any thoughts would be greatly appreciated!

Answered by andrewbuilder in 681540022

As hinted in the error message, you're attempting to cast the type ServiceCategory in a manner that SwiftUI cannot manage.

The var sectionIdentifier is expecting a reference to a ServiceCategory as shown in this line

) var sectionedServices: SectionedFetchResults<ServiceCategory?, Service>

but instead you are passing the faulted object ID _NSCoreDataTaggedObjectID per your line

sectionIdentifier: \Service.serviceCategory,

Service.serviceCategory holds a reference to an entity instance... in your situation the ServiceCategory entity. If you place a break point in your code and query the value you'll probably see a reference to a record id for that entity, not the actual entity object.

Frankly I'd recommend that you change your approach a little write an extension on your Service entity and include a convenience method to grab this value...

extension Service: NSManagedObject {
    @objc var serviceCategoryName: String {        
        return self.serviceCategory.name ?? "NO NAME"
    }
}

then use this as follows...

@SectionedFetchRequest(
    sectionIdentifier: \.serviceCategoryName,
    sortDescriptors: [
      SortDescriptor(\.active, order: .reverse),
      SortDescriptor(\.displayText)
    ],
    predicate: NSPredicate(format: "%K = %d", #keyPath(.active), true),
    animation: .default
  ) var sectionedServices: SectionedFetchResults<String, Service>

Note the change from type ServiceCategory to type String.

and in the list...

ForEach(sectionedServices) { section in
  Section(header: Text(section.id)) {
    ForEach(section) { service in
      Text(service.displayText ?? "")
    }
  }
}

Coredata entities : Type (with attribute: "nom") who own many Sujet (with attribute: "nom") (one-to-many relationship)

This works for me :

List of Sujets, sectioned by Types

struct SujetsSectionedListByType: View {

    // MARK: - Properties
    @SectionedFetchRequest(
        sectionIdentifier: \Sujet.type?.nom,
        sortDescriptors: [
            SortDescriptor(\Sujet.type?.nom, order: .forward),
            SortDescriptor(\Sujet.nom, order: .forward)
        ])
    private var sujets: SectionedFetchResults<String?, Sujet>

    @Binding var selectedSujet: Sujet?

    // MARK: - Body
    var body: some View {
        List(selection: $selectedSujet) {
            ForEach(sujets) { section in
                Section(header: Text(section.id ?? "")) {
                    ForEach(section) { sujet in
                        NavigationLink(destination: SujetDetail(sujet: sujet, selectedSujet: $selectedSujet), tag: sujet, selection: $selectedSujet) {
                            SujetListItem(sujet: sujet)
                        }
                    }
                }
            }
        }
        .frame(minWidth: ListsMinWidth)
    }

}

A workaround to get the whole managedObject, not only one of her string attributes :

    @SectionedFetchRequest(
        sectionIdentifier: \Sujet.type?.objectID,
        sortDescriptors: [
            SortDescriptor(\Sujet.type?.nom, order: .forward),
            SortDescriptor(\Sujet.nom, order: .forward)
        ])
    var sujets: SectionedFetchResults<NSManagedObjectID?, Sujet>

and

    List(selection: $selectedSujet) {
            ForEach(sujets) { section in
                Section(header: HStack {
                    let type = context.object(with: section.id!) as! Type
                    Label(title: { Text(type.wrappedNom) },
                          icon: { type.wrappedIcon
                            .resizable()
                            .scaledToFit()
                            .frame(width: 16, height: 16)
                    })
                }) {
                    ForEach(section) { sujet in
                        NavigationLink(destination: SujetDetail(sujet: sujet, selectedSujet: $selectedSujet), tag: sujet, selection: $selectedSujet) {
                            SujetListItem(sujet: sujet)
                        }
                    }
                }
            }
        }

I can't find possible to get the managedObject from 'section.id', which is a '_NSCoreDataTaggedObjectID', if sectionIdentifier is passed a 'Type?'

Accepted Answer

As hinted in the error message, you're attempting to cast the type ServiceCategory in a manner that SwiftUI cannot manage.

The var sectionIdentifier is expecting a reference to a ServiceCategory as shown in this line

) var sectionedServices: SectionedFetchResults<ServiceCategory?, Service>

but instead you are passing the faulted object ID _NSCoreDataTaggedObjectID per your line

sectionIdentifier: \Service.serviceCategory,

Service.serviceCategory holds a reference to an entity instance... in your situation the ServiceCategory entity. If you place a break point in your code and query the value you'll probably see a reference to a record id for that entity, not the actual entity object.

Frankly I'd recommend that you change your approach a little write an extension on your Service entity and include a convenience method to grab this value...

extension Service: NSManagedObject {
    @objc var serviceCategoryName: String {        
        return self.serviceCategory.name ?? "NO NAME"
    }
}

then use this as follows...

@SectionedFetchRequest(
    sectionIdentifier: \.serviceCategoryName,
    sortDescriptors: [
      SortDescriptor(\.active, order: .reverse),
      SortDescriptor(\.displayText)
    ],
    predicate: NSPredicate(format: "%K = %d", #keyPath(.active), true),
    animation: .default
  ) var sectionedServices: SectionedFetchResults<String, Service>

Note the change from type ServiceCategory to type String.

and in the list...

ForEach(sectionedServices) { section in
  Section(header: Text(section.id)) {
    ForEach(section) { service in
      Text(service.displayText ?? "")
    }
  }
}
@SectionedFetchRequest using relationship as sectionIdentifier
 
 
Q