SwiftUI, Table and the multiple row selection problem (macos).

I have a simple code (I'm a beginner).  In the code below, row selection is not working. A single row is selected and unselected immediately, multiple rows or doubleclick ends with an error:

CoreDataTable[52390:1238512] Fatal error: Duplicate elements of type 'Optional' were found in a Set. This usually means either that the type violates Hashable's requirements, or that members of such a set were mutated after insertion.

Single line selection (@State private var selection:Item.ID) works correctly.

What am I doing wrong? Thanks for any advice.

Ludek.

import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Item.name, ascending: true)],
        animation: .default)

    private var items: FetchedResults<Item>

    @State private var selection = Set<Item.ID>()

    var body: some View {
        Table(items, selection: $selection) {
            TableColumn("Name") {
                   Text($0.name ?? "NoName")
            }
        }
        .padding()
        .toolbar {
            Button(action: addItem) {
                Label("Add Item", systemImage: "plus")
            }
        }
    }

    private func addItem() {
        withAnimation {
            let newItem = Item(context: viewContext)
            let count = items.count + 1
            newItem.id = UUID()
            newItem.name = "Test nr.: \(count)"
            do {
                try viewContext.save()
            } catch {
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
}
Answered by Ludek66 in 711685022

Error found, CoreData editor generates code:

    @nonobjc public class func fetchRequest() -> NSFetchRequest<Item> {
        return NSFetchRequest<Item>(entityName: "Item")
    }
    @NSManaged public var id: UUID?
    @NSManaged public var name: String?
}

but the UUID is not optional, so correction: @NSManaged public var id: UUID

and the table works as expected.

Could you show the complete code so that we can test ? In particular, how did you define struct Item ? Did you declare it Identifiable ? Is it Hashable ?

It is a standard generated new Core Data project, only the attributes of the Item entity are modified.

extension Item: Identifiable { @nonobjc public class func fetchRequest() -> NSFetchRequest<Item> { return NSFetchRequest<Item>(entityName: "Item") } @NSManaged public var id: UUID? @NSManaged public var name: String? }

Here's the complete code (CoreDataTableApp.swift):

import CoreData

@main
struct CoreDataTableApp: App {
    let persistenceController = PersistenceController.shared

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.managedObjectContext, persistenceController.container.viewContext)
        }
    }
}

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Item.name, ascending: true)],
        animation: .default)
    
    private var items: FetchedResults<Item>
    
    @State private var selection = Set<Item.ID>()
    
    var body: some View {
        Table(items, selection: $selection) {
            TableColumn("Name") {
                   Text($0.name ?? "NoName")
            }
        }
        .padding()
        .toolbar {
            Button(action: addItem) {
                Label("Add Item", systemImage: "plus")
            }
        }
    }
    
    private func addItem() {
        withAnimation {
            let newItem = Item(context: viewContext)
            let count = items.count + 1
            newItem.id = UUID()
            newItem.name = "Test Name \(count)"

            do {
                try viewContext.save()
            } catch {
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
}
// MARK: Core Data 
@objc(Item)
public class Item: NSManagedObject {}

extension Item: Identifiable {
    @nonobjc public class func fetchRequest() -> NSFetchRequest<Item> {
        return NSFetchRequest<Item>(entityName: "Item")
    }
    @NSManaged public var id: UUID?
    @NSManaged public var name: String?
}

struct PersistenceController {
    static let shared = PersistenceController()

    let container: NSPersistentContainer

    init(inMemory: Bool = false) {
        container = NSPersistentContainer(name: "CoreDataTable")
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        container.viewContext.automaticallyMergesChangesFromParent = true
    }
}

Accepted Answer

Error found, CoreData editor generates code:

    @nonobjc public class func fetchRequest() -> NSFetchRequest<Item> {
        return NSFetchRequest<Item>(entityName: "Item")
    }
    @NSManaged public var id: UUID?
    @NSManaged public var name: String?
}

but the UUID is not optional, so correction: @NSManaged public var id: UUID

and the table works as expected.

Hi Ludek66,

I still got the same problem. I thought, the optional of field id can be set in inspector, too. But unfortunately that doesn't help. Where is this file? Or is there another thing to be set? And how can I access the Item to display the name e.g. or make changes / open editing for this set?

Set up manual generation and then generate the class. See attached screenshot.

Manually generating the NSManagedObject and removing the optional can cause more issues. I did this and when trying to delete from the table, I hit an exception, because it's trying to access a nil id property, even though I've removed it from the context.

My solution was to name the attribute itemId and then create an extension on Item and create a read-only id: UUID property.

extension Item {
    public var id: UUID {
        itemId ?? UUID() // unfortunately I needed to provide a default here, which seems like a hacky solution.
    }
}

At least in this way you don't have to bother with manually generating the subclasses either.

SwiftUI, Table and the multiple row selection problem (macos).
 
 
Q