Implementing filtering through checkbox selection in SwiftUI list EditMode with CoreData

119 views Asked by At

I am trying to implement a behavior similar to Apple's iOS Mail app, with a list showing CoreData entries that have their attribute 'showing' set to 'true'. When entering editMode, the list shows all CoreData entries, and shows the checkboxes of the 'showing' entries as checked.

The user can change the selection, and upon exiting editMode, changes for 'showing' are written to CoreData, and the new list of previously selected entries is shown.

Issue 1: When entering editMode, the missing entries slide in first, and only after all entries are showing do the 'showing' ones get checked off. Like in Mail, checkboxes for 'showing' entries should slide in already checked. (When all entries are checked, I get the desired slide in behavior.)

Issue 2: Newly selected entries jumping when exiting editMode, and

Issue 3: Multiple rows becoming selected even though not in editMode (and changing the 'showing' Bool).

enter image description hereenter image description here

I expect the entries that have their 'showing' attribute set to 'true', to be already checked off as the checkboxes slide into view. The iOS Mail app does this correctly.

This is my current implementation:

import SwiftUI


struct ContentView: View {
    
    @Environment(\.managedObjectContext) var moc
    @FetchRequest(
        entity: Animal.entity(),
        sortDescriptors: [NSSortDescriptor(keyPath: \Animal.name, ascending: true)]
    ) var allAnimals: FetchedResults<Animal>

    @State private var selection = Set<Animal>()
    @State var mode: EditMode = .inactive

    var animals: [Animal] {
        if mode == .active {
            return Array(allAnimals)
        } else {
            return allAnimals.filter { $0.showing }
        }
    }
    
    var body: some View {
        NavigationView {
            List(selection: $selection) {
                ForEach(animals, id: \.self) { animal in
                    NavigationLink(destination: Text("Detail View")) {
                        Label(animal.name!, systemImage: animal.symbol ?? "folder")
                    }
                    .listRowBackground(mode == .active ? Color(UIColor.secondarySystemGroupedBackground) : nil)
                }
                

                
            }
            .onChange(of: mode) { newMode in
                if newMode == .active {
                    selection = Set(allAnimals.filter { $0.showing })
                }
                if newMode == .inactive {
                    updateAnimals()
                }
            }

            .navigationTitle("Animals")

            .safeAreaInset(edge: .bottom) {
                Button(action: {
                    let lion = Animal(context: moc)
                    lion.name = "Lion"
                    lion.symbol = "01.circle"
                    lion.showing = true
                    lion.id = UUID()

                    let tiger = Animal(context: moc)
                    tiger.name = "Tiger"
                    tiger.symbol = "02.circle"
                    tiger.showing = true
                    tiger.id = UUID()

                    let zebra = Animal(context: moc)
                    zebra.name = "Zebra"
                    zebra.symbol = "03.circle"
                    zebra.showing = true
                    zebra.id = UUID()

                    let elephant = Animal(context: moc)
                    elephant.name = "Elephant"
                    elephant.symbol = "04.circle"
                    elephant.showing = true
                    elephant.id = UUID()

                    let giraffe = Animal(context: moc)
                    giraffe.name = "Giraffe"
                    giraffe.symbol = "05.circle"
                    giraffe.showing = true
                    giraffe.id = UUID()

                    try? moc.save()
                }, label: {
                    Label("Add animals", systemImage: "plus.circle")
                })
                .buttonStyle(.plain)
                .foregroundColor(.accentColor)
                .padding()
                .frame(maxWidth: .infinity, alignment: .leading)
            }

            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    EditButton()
                }                  
            }

            .environment(\.editMode, $mode)
        }
    }
    
    func updateAnimals() {
        allAnimals.forEach { $0.showing = false }
        
        for selectedAnimal in selection {
            selectedAnimal.showing = true
        }
        
        if moc.hasChanges {
            do {
                try moc.save()
            } catch {
                print("Error saving managed object context: \(error)")
            }
        }
    }
    
}

I have included an "Add animals" button to add sample data and show my CoreData entity attributes.

How do I implement the filtering of a list with editMode that works as expected?

1

There are 1 answers

0
malhal On

onChange is for an external action not for updating another state, you'll get glitches if you do that.

What you probably should do is change the @FetchRequest predicate from one that searches for showing and nil predicate (that returns all), e.g.

func updatePredicate() {
    animals.nsPredicate = mode == .active ? nil : NSPredicate(format: "showing = true") // best make this predicate static somewhere
}

var body: some View {
    let _ = updatePredicate()
        NavigationView {
            List(selection: $selection) {
                ForEach(animals) { animal in