由 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
得到修复。 EditButton
和 List
使用的 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
、.toolbar
、ToolBarItem
、Menu
,直到 Picker
,您才真正使用流派。制作一个创建流派并立即在主体中使用它的结构会更有效。例如。您可以创建一个 GenrePicker
结构来执行 FetchRequest
并首先调用 Picker
,如果您在外部需要它,则将绑定传递给选择。
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)
}
但是用户无法再编辑列表顺序。我猜想可以将列表的重新索引推迟到用户退出编辑模式的时间点。但是我无法按照建议
此问题的根本原因是什么,解决它的最佳方法是什么?
我注意到,如果您稍微向下拖动 sheet 移动一行后,table 单元格 re-enter 编辑模式与编辑按钮相匹配。如果您再次向下拖动 sheet 单元格,然后退出编辑模式!这让我觉得当 List
在 sheet 内部时存在错误,我将其报告为 FB9969447。我相信这在您的测试项目中也会发生的原因是因为 GenreManager()
在移动完成时初始化,其原因在下面解释。作为解决方法,您可以使用 fullScreenCover
直到 sheet
得到修复。 EditButton
和 List
使用的 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
、.toolbar
、ToolBarItem
、Menu
,直到 Picker
,您才真正使用流派。制作一个创建流派并立即在主体中使用它的结构会更有效。例如。您可以创建一个 GenrePicker
结构来执行 FetchRequest
并首先调用 Picker
,如果您在外部需要它,则将绑定传递给选择。