List(selection: Binding<Set<>>) not working with CoreData

Hi, I am new to developpement in xcode and am trying to build my first app in SwiftUI.

I am struggling with multi selection in a list of CoreData Items.


I build an application comparing the functionality when using an Array versus when using CoreData

The Array tab is working as I expect but the Core data tab is not.

Am I doing something wrong or is it a bug?


You can see further details/comments in the code.


Thanks,


import SwiftUI


// ########################################################################################
// XCode
//   Version 11.4.1 (11E503a)
// Simulator
//   Version 11.4.1 (921.9)
//   SimulatorKit 581.9.1
//   CoreSimulator 704.12.1
// Device
//   iPhone SE (2nd generation)
// ########################################################################################



// ########################################################################################
// the CoreData entity is named MyNumber
// I don't think extending it as Identifiable has any effect but it shouldn't hurt to add it
// ########################################################################################
extension MyNumber: Identifiable {
    // ########################################################################################
    // the entity has these two fields
    // @NSManaged public var id: UUID?
    // @NSManaged public var stringValue: String?
    // ########################################################################################
}

struct ContentView: View {
    
    // Tab variables
    @State private var tabSelection = 0
    
    // Array variables
    @State var arrayNumbers = ["One","Two","Three","Four","Five","Six","Seven","Eight","Nine","Ten"]
    @State var arrayEditMode = EditMode.inactive
    @State var arraySelection = Set<String>()
    
    // Core Data variables
    @State var cdEditMode = EditMode.inactive
    // ########################################################################################
    // Am I using the correct type ? Set<MyNumber>() : I also tried Set<UUID>() without success
    // ########################################################################################
    // # Using Set<MyNumber>()
    @State var cdSelection = Set<MyNumber>()
    // ########  OR  ############
    // # Using Set<UUID>()
    // @State var cdSelection = Set<UUID>()
    // #####################################################################
    @Environment(\.managedObjectContext) var moc
    @FetchRequest(entity: MyNumber.entity(), sortDescriptors:[]) var cdNumbers: FetchedResults<MyNumber>

    
    
    var body: some View {
        
        TabView(selection: $tabSelection){
            // Array Tab
            VStack{
                Text("Array").font(.largeTitle)
                NavigationView {
                    
                    List(selection: $arraySelection) {
                        ForEach(arrayNumbers, id: \.self) { number in
                            Text(number)
                        }
                        .onDelete(perform: arrayOnSwipeDelete)
                    }
                    .navigationBarTitle("Selected count \(arraySelection.count)")
                    .navigationBarItems(leading: arrayDeleteButton, trailing: EditButton())
                    .environment(\.editMode, self.$arrayEditMode)
                }
            }
            .tabItem {
                VStack {
                    Image(systemName: "checkmark")
                    Text("Array")
                    
                }
            }
            .tag(0)
            
            // Core Data Tab
            
            VStack{
                VStack{
                    Text("Core Data").font(.largeTitle)
                    Button("Add coredata numbers") {
                        for number in self.arrayNumbers {
                          let cdNumber = MyNumber(context: self.moc)
                           cdNumber.id = UUID()
                           cdNumber.stringValue = "\(number)"
                        }
                        try? self.moc.save()
                    }.disabled(cdEditMode.isEditing)
                }
                NavigationView {
                    List(selection: $cdSelection) {
                        ForEach(cdNumbers, id: \.id) { number in
                            Text(number.stringValue ?? "NULL")
                        }
                        // #####################################################################
                        // Problem #1
                        // Without the onDelete, the left selection column does not appear at all
                        // To reproduce this
                        // 1 - click "Add coredata records"
                        // 2 - Click "Edit"
                        // #####################################################################
                        // Problem #2
                        // With the onDelete, rows can be selected but $cdSelection stays empty
                        // To reproduce this
                        // 1 - click "Add coredata records"
                        // 2 - Click "Edit"
                        // 3 - Select records (notice the selection count is not updated)
                        // 4 - Even with some selected records, the trashcan stays disabled because of the disabled event
                        // 5 - Commenting out the disabled event and clicking on the trashcan should delete records but it does nothing
                        // #####################################################################
                        .onDelete(perform: cdOnSwipeDelete)
                    }
                    .navigationBarTitle("Selected count \(cdSelection.count)")
                    .navigationBarItems(leading: cdDeleteButton, trailing: EditButton())
                    .environment(\.editMode, self.$cdEditMode)
                }}
                .tabItem {
                    VStack {
                        Image(systemName: "xmark")
                        Text("CoreData")
                    }
            }
            .tag(1)
            
        }
        
    }
    init() {
        // Global Navigation bar customizations
        UINavigationBar.appearance().largeTitleTextAttributes = [
            .font : UIFont.systemFont(ofSize: 18, weight: UIFont.Weight.bold)]
    }
    
    
    // array functions
    private var arrayDeleteButton: some View {
        return Button(action: arrayDeleteNumbers) {
            if arrayEditMode == .active {
                Image(systemName: "trash")
            }
        }.disabled(arraySelection.count <= 0)
    }
    
    private func arrayDeleteNumbers() {
        for id in arraySelection {
            if let index = arrayNumbers.lastIndex(where: { $0 == id })  {
                arrayNumbers.remove(at: index)
            }
        }
        arraySelection = Set<String>()
    }
    
    // #####################################################################
    // Array selecction is not affected by the presence of the arrayOnSwipeDelete function
    // #####################################################################
    private func arrayOnSwipeDelete(with indexSet: IndexSet) {
        indexSet.forEach { index in
            arrayNumbers.remove(at: index)
        }
    }
    
    
    // Core Data Functions
    private var cdDeleteButton: some View {
        return Button(action: cdDeleteNumbers) {
            if cdEditMode == .active {
                Image(systemName: "trash")
            }
        // #####################################################################
        // Even with a commented .disable rule, the trash can deletes nothing
        // #####################################################################
        }.disabled(cdSelection.count <= 0)

    }
    
    private func cdDeleteNumbers() {
        for selectedItem in self.cdSelection{
            // #####################################################################
            // # Using Set<MyNumber>()
            self.moc.delete(selectedItem)
            // ########  OR  ############
            // # Using Set<UUID>()
            // self.moc.delete(cdNumbers.first(where:  {$0.id == selectedItem})!)
            // #####################################################################
        }
        try? self.moc.save()
        
        // #####################################################################
        // # Using Set<MyNumber>()
        cdSelection = Set<MyNumber>()
        // ########  OR  ############
        // # Using Set<UUID>()
        // cdSelection = Set<UUID>()
        // #####################################################################
    }
    
    // #####################################################################
    // Core data selecction is affected by the presence of the cdOnSwipeDelete function
    // IE: The selection column is shown in edit mode only if
    // this functuion is referenced by ForEach{}.onDelete()
    // #####################################################################
    private func cdOnSwipeDelete(with indexSet: IndexSet) {
        
        indexSet.forEach { index in
            let number = cdNumbers[index]
            self.moc.delete(number)
        }
        try? self.moc.save()
    }
    
    
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Accepted Reply

I could have confirmed that your code compiled and worked as you described.


And changing line 99. as follows, your Problem #2 did not happen:

                        ForEach(cdNumbers, id: \.self) { number in

(Used `Set<MyNumber>` version lines in other parts.)

Replies

Does this code compile ?

Don't you get an error line 57 ?


Note: you should move your post to SwiftUI section.

Hi thanks for the reply.


The code does compile fine over here but as the comments say, you need to create the MyNumber entity manually in the data model.


I have now found the SwiftUI section under SDK and will look there for possible answers.

In the meantime, has anyone been able to reproduce the issue I am describing.



Thanks,

I could have confirmed that your code compiled and worked as you described.


And changing line 99. as follows, your Problem #2 did not happen:

                        ForEach(cdNumbers, id: \.self) { number in

(Used `Set<MyNumber>` version lines in other parts.)

Thank you OOPer,


This is exactly what I was looking for to fix the second problem.


As for the first problem, I described it in case it could lead to a solution to problem #2 but since I am planning to use the .onDelete in my applicationanyway, everything is good.


Thanks again