Home > Back-end >  SwiftUI List backed by FetchedResults wrongly exits out of an edit mode
SwiftUI List backed by FetchedResults wrongly exits out of an edit mode

Time:03-30

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?

CodePudding user response:

I noticed that after you move a row if you drag the sheet down slightly, the table cells re-enter edit mode to match the edit button. If you drag the sheet down another time the cells then exit edit mode! This makes me think there is a bug when List is inside of a sheet, which I reported as FB9969447. I believe the reason this also happens in your test project is because GenreManager() is init when a move is done, which the reason for is explained below. As a workaround you could use fullScreenCover until sheet is fixed. The editMode that EditButton and List use is part of the environment and sheets have always behaved a bit weird with environment vars so that is probably the reason for the bug. You could also attempt to re-architect your View structs so that GenreManager() is not init when genres is changed but that is probably futile given the bug also occurs when the sheet is dragged.

SwiftUI features dependency tracking so if you don't call ForEach(genres) it no longer runs body when genres changes. So the problem isn't to do with the Picker itself, just the fact that body is being called in BookList when a move is made causing a change to genres. At the top of body use let _ = Self._printChanges() you'll see debug output that tells you the reason for running body. FYI there currently a bug where a View init with @FetchRequest (even with same params) always has body called because of @self changed - it's because that struct inits a new object instead of using @StateObject so SwiftUI always thinks the View has changed FB9956812.

So I think what is happening is when the genre list is changed by the move, BookList calls body (because genres is used in the Picker) and it inits a GenreManager.

Here is a SwiftUI tip, it's best to restructure your Views so you aren't initing too many layers of things that don't use the data that SwiftUI calls body when it detects changes. I.e. in your BookList when genres changes and body is called you create a NavigationView, .toolbar, ToolBarItem, Menu and it isn't until Picker that you actually use the genres. It's more efficient to make a struct that creates genres and uses it immediately in body. E.g. you could make a GenrePicker struct that does the FetchRequest and calls Picker first, pass in a binding to the selection if you need it outside.

  • Related