The sample project is an app that allows people to track the books they've read and their genres. To display the books, we have BookList
view.
struct BookList: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
sortDescriptors: [SortDescriptor(\.index), SortDescriptor(\.name)],
animation: .default
) private var genres: FetchedResults<Genre>
@State private var filter: Genre?
@State private var isPresentingGenreManager = false
var body: some View {
NavigationView {
Text("BookList")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Genres") {
isPresentingGenreManager = true
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Menu {
Picker("Filtering options", selection: $filter) {
#warning("This is the code that is causing the issue.")
ForEach(genres) { genre in
let tag = genre as Genre?
Text(genre.name ?? "").tag(tag)
}
}
} label: {
Label("Filter", systemImage: "line.3.horizontal.decrease.circle")
}
}
}
.sheet(isPresented: $isPresentingGenreManager, onDismiss: {
try? viewContext.save()
}) {
GenreManager()
}
}
}
}
If the user wants to manage the genres of their book collection, they're invited to the GenreManager
view.
struct GenreManager: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
sortDescriptors: [SortDescriptor(\.index), SortDescriptor(\.name)],
animation: .default
) private var genres: FetchedResults<Genre>
var body: some View {
NavigationView {
List {
ForEach(genres) { genre in
HStack {
Text(genre.name ?? "")
Spacer()
Text("index: \(genre.index)")
.foregroundColor(.secondary)
}
}
.onMove(perform: moveGenres)
}
.navigationTitle("Genres")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
EditButton()
}
ToolbarItem(placement: .primaryAction) {
Button(action: {
let genre = Genre(context: viewContext)
genre.name = genreNames.randomElement()!
do {
try viewContext.save()
} catch {
viewContext.rollback()
}
}, label: { Label("Add", systemImage: "plus") })
}
}
}
}
// The function to move items in a list backed by Core Data
// Reference: https://stackoverflow.com/questions/59742218/swiftui-reorder-coredata-objects-in-list
private func moveGenres(from source: IndexSet, to destination: Int) {
var revisedItems: [Genre] = genres.map { $0 }
revisedItems.move(fromOffsets: source, toOffset: destination)
for reverseIndex in stride(from: revisedItems.count - 1, through: 0, by: -1) {
revisedItems[reverseIndex].index = Int64(reverseIndex)
}
}
}
Genre
is a simple Core Data entity that has a name of type String
and an index to persist the order in a list of type Int64
. I want to give the users an option to reorder the list using a simple algorithm that I took from here. So far so good.
Now I want to add a filter to the BookList
so users can filter their list based on the genres that they have in place. But as soon as I add this piece of code an obscure issue presents itself.
ForEach(genres) { genre in
let tag = genre as Genre?
Text(genre.name ?? "").tag(tag)
}
As I now try to reorder the list in GenreManager
, as soon as I move a single item, the list bails out of an edit mode and EditButton
is left in an incorrect state.
I'm expecting the user to be able to comfortably edit the list without interruption caused by the list exiting out of edit mode arbitrarily.
I've tried to share fetched results from BookList
superview to GenreManager
subview as seen here, but that does not solve the issue. What does solve the issue is removing this code from moveGenres
function.
for reverseIndex in stride(from: revisedItems.count - 1, through: 0, by: -1) {
revisedItems[reverseIndex].index = Int64(reverseIndex)
}
But then the user isn't able to edit the list order any more. I'm guessing it's possible to defer the reindexing of the list to the point where the user quits editing mode. But I wasn't able to successfully observe edit mode changes in onChange as suggested here. And I consider building a custom edit button as a last resort since it's already provided as a native solution by Apple.
What is the root cause of this issue and what is the best way to fix it?