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).
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?
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 forshowing
and nil predicate (that returns all), e.g.