由 FetchedResults 支持的 SwiftUI 列表错误地退出了编辑模式

SwiftUI List backed by FetchedResults wrongly exits out of an edit mode

The sample project 是一款允许人们跟踪他们读过的书籍及其类型的应用程序。要显示书籍,我们有 BookList 个视图。

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()
                }
        }
    }
}

如果用户想要管理其图书的类型 collection,他们将被邀请进入 GenreManager 视图。

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: 
    private func moveGenres(from source: IndexSet, to destination: Int) {
        var revisedItems: [Genre] = genres.map { [=11=] }
        revisedItems.move(fromOffsets: source, toOffset: destination)
        for reverseIndex in stride(from: revisedItems.count - 1, through: 0, by: -1) {
            revisedItems[reverseIndex].index = Int64(reverseIndex)
        }
    }
}

Genre 是一个简单的 Core Data 实体,它有一个类型为 String 的名称和一个用于将顺序保存在类型为 Int64 的列表中的索引。我想为用户提供一个选项,让他们可以使用我从 中获取的简单算法对列表进行重新排序。到目前为止一切顺利。

现在我想向 BookList 添加一个过滤器,这样用户就可以根据他们已有的流派来过滤他们的列表。但是一旦我添加了这段代码,一个模糊的问题就出现了。

ForEach(genres) { genre in
    let tag = genre as Genre?
    Text(genre.name ?? "").tag(tag)
}

当我现在尝试对 GenreManager 中的列表重新排序时,一旦我移动单个项目,列表就会退出编辑模式并且 EditButton 处于不正确的状态。

我希望用户能够舒适地编辑列表,而不会因列表任意退出编辑模式而中断。

我尝试将从 BookList 超级视图获取的结果共享到 GenreManager 子视图,如 所示,但这并没有解决问题。解决问题的方法是从 moveGenres 函数中删除此代码。

for reverseIndex in stride(from: revisedItems.count - 1, through: 0, by: -1) {
    revisedItems[reverseIndex].index = Int64(reverseIndex)
}

但是用户无法再编辑列表顺序。我猜想可以将列表的重新索引推迟到用户退出编辑模式的时间点。但是我无法按照建议 在 onChange 中成功观察到编辑模式的变化。我考虑将构建自定义编辑按钮作为最后的手段,因为它已作为 Apple 的本机解决方案提供。

此问题的根本原因是什么,解决它的最佳方法是什么?

我注意到,如果您稍微向下拖动 sheet 移动一行后,table 单元格 re-enter 编辑模式与编辑按钮相匹配。如果您再次向下拖动 sheet 单元格,然后退出编辑模式!这让我觉得当 List 在 sheet 内部时存在错误,我将其报告为 FB9969447。我相信这在您的测试项目中也会发生的原因是因为 GenreManager() 在移动完成时初始化,其原因在下面解释。作为解决方法,您可以使用 fullScreenCover 直到 sheet 得到修复。 EditButtonList 使用的 editMode 是环境的一部分,而 sheets 总是对环境变量表现得有点奇怪,所以这可能是错误的原因。您还可以尝试 re-architect 您的视图结构,以便 GenreManager()genres 更改时不会初始化,但这可能是徒劳的,因为在拖动 sheet 时也会发生错误.

SwiftUI 具有依赖项跟踪功能,因此如果您不调用 ForEach(genres),它在 genres 更改时不再运行主体。所以问题不在于 Picker 本身,而在于当移动导致 genres 发生变化时,正文在 BookList 中被调用。在 body 的顶部使用 let _ = Self._printChanges() 你会看到调试输出告诉你 运行 body 的原因。仅供参考,目前有一个错误,其中带有 @FetchRequest 的 View init(即使具有相同的参数)总是因为 @self changed 而调用 body - 这是因为该结构初始化了一个新对象而不是使用 @StateObject 所以SwiftUI 总是认为 View 已经改变 FB9956812.

所以我认为发生的事情是当流派列表被移动更改时,BookList 调用正文(因为选择器中使用了流派)并且它初始化了一个 GenreManager

这里有一个 SwiftUI 提示,最好重构你的视图,这样你就不会启动太多层,这些东西不使用 SwiftUI 在检测到变化时调用 body 的数据。 IE。在您的 BookList 中,当流派发生变化并调用 body 时,您创建了一个 NavigationView.toolbarToolBarItemMenu,直到 Picker,您才真正使用流派。制作一个创建流派并立即在主体中使用它的结构会更有效。例如。您可以创建一个 GenrePicker 结构来执行 FetchRequest 并首先调用 Picker,如果您在外部需要它,则将绑定传递给选择。