SwiftUI 列表在删除后继续显示核心数据项

Posted

技术标签:

【中文标题】SwiftUI 列表在删除后继续显示核心数据项【英文标题】:SwiftUI List Keeps Showing Core Data Items After They Were Deleted 【发布时间】:2021-06-19 10:37:49 【问题描述】:

我正在探索 SwiftUI,但遇到了一个问题。我有一个使用 Core Data 保存的歌手列表

let singer1 = Singer(context: viewContext)
singer1.firstName = "Taylor"
singer1.lastName = "Swift"

let singer2 = Singer(context: viewContext)
singer2.firstName = "Ed"
singer2.lastName = "Sheeran"

let singer3 = Singer(context: viewContext)
singer3.firstName = "Adele"
singer3.lastName = "Adkins"

try? viewContext.save()

我创建了另一个视图来填充歌手列表。

struct ListOfSingers<T: NSManagedObject, Content: View>: View 
@Environment(\.managedObjectContext) private var viewContext

@FetchRequest<T> var singers: FetchedResults<T>

let content: (T) -> Content
init(filterKey: String, filterValue: String, @ViewBuilder content: @escaping (T) -> Content) 
    _singers = FetchRequest<T>(entity: T.entity(), sortDescriptors: [], predicate: NSPredicate(format: "%K BEGINSWITH %@", filterKey, filterValue))
    self.content = content
    


var body: some View 
    
    List 
        ForEach(singers, id: \.self) 
            content($0)
        .onDelete(perform: deleteSinger)
    
    


func deleteSinger(at offsets: IndexSet) 
    withAnimation 
        
        offsets.map  singers[$0] .forEach(viewContext.delete)
        
        try? viewContext.save()
        
    

我使用谓词将它们分类为 2 个列表。我无法解决的问题是,当我使用谓词“S”并删除列表中的对象,然后返回谓词“A”并删除该列表中的歌手时,即使我检查了歌手,我仍然有歌手显示在那里' 数组,它说它是空的。我也可以与这个“幽灵”对象交互,但是如果我尝试删除这个对象,应用程序会崩溃,因为现在它知道数组是空的并且有这样一个对象要删除。

这是我的内容视图:

struct ContentView: View 
@Environment(\.managedObjectContext) private var viewContext

@State var lastNameFilter = "A"

var body: some View 
    NavigationView 
        VStack 
            ListOfSingers(filterKey: "lastName", filterValue: lastNameFilter)  (singer: Singer) in
                Text("\(singer.wrappedName) \(singer.wrappedLastName)")
                    .onAppear  print( "\(singer.wrappedName) \(singer.wrappedLastName)" )
            

            Button("Add Singers") 
                let singer1 = Singer(context: viewContext)
                singer1.firstName = "Taylor"
                singer1.lastName = "Swift"

                let singer2 = Singer(context: viewContext)
                singer2.firstName = "Ed"
                singer2.lastName = "Sheeran"

                let singer3 = Singer(context: viewContext)
                singer3.firstName = "Adele"
                singer3.lastName = "Adkins"

                try? viewContext.save()
            
            .frame(width: 280, height: 50)
            .background(Color.blue)
            .foregroundColor(.white)
            .cornerRadius(10)
            .padding()

            HStack 
                Button("Sort by A") 
                    withAnimation 
                        lastNameFilter = "A"
                    
                
                .frame(width: 130, height: 50)
                .background(Color.blue)
                .foregroundColor(.white)
                .cornerRadius(10)

                Spacer()

                Button("Sort by S") 
                    withAnimation 
                        lastNameFilter = "S"
                    
                
                .frame(width: 130, height: 50)
                .background(Color.blue)
                .foregroundColor(.white)
                .cornerRadius(10)
            
            .padding()
        
        .navigationTitle("Singers") 

【问题讨论】:

您只执行一次获取请求,因此 UI 不会更新。保存后再次手动执行它,或收听上下文的更新通知或采用新的@FetchRequest 属性包装器 我采用了新的@FetchRequest 属性包装器,但结果仍然相同。我尝试再次手动执行 fetch 请求,但它失败了,因为 SwiftUI 不允许使用变异函数,因为 @FetchRequest varsingers: FetchedResults 是不可变的。你有其他想法吗? 跳过这个过于复杂的通用 ListOfSingers 并将获取请求和其他代码移动到 ContentView 中,看看它是否正常工作 我将获取请求保留在另一个视图中,因为我希望它被动态过滤,因此我可以传递不同的谓词和 NSSortDescriptor。如果我在 ContentView 中获取请求,我将无法使其动态化,例如 @State var lastNameFilter = "A" 在初始化之前无法使用。 另外,即使删除了泛型,我仍然遇到同样的问题 【参考方案1】:

问题是由于某种原因 ListOfSingers 结构被调用了两次。即使我从 Core Data 中删除了最后一位歌手,渲染时间也比重新索引 Core Data 更快。所以我最终得到了一个“幽灵视图”,它是在我的项目从核心数据中完全删除之前呈现的。当我尝试删除 Ghost 视图时,我遇到了一个不存在的数组。它总是会导致应用崩溃。

解决方案是添加一个函数,在删除触发后立即检查 Core Data 中是否有任何项目。

    在主视图中创建@State var noMoreSingers = false 以检查应呈现哪个视图并将此条件添加到 VStack 以明确告知应呈现哪个视图:

     VStack 
         if !noMoreSingers 
             ListOfSingers(filterKey: "lastName", filterValue: lastNameFilter)  (singer: Singer) in
                 Text("\(singer.wrappedName) \(singer.wrappedLastName)")
                else 
                 Spacer()
             
    

    @Binding var noMoreSingers: Bool 添加到ListOfSingers 以便能够在Core Data 中没有更多项目时告诉主视图

    添加函数到 Persistanse Controller 以获取所有项目:

         func getAllSingers() -> [Singer] 
         let request: NSFetchRequest<Singer> = Singer.fetchRequest()
    
         do 
             return try container.viewContext.fetch(request)
          catch 
             fatalError("Error Fetching Users")
         
     
    

    将函数添加到 ListOfSingers 以触发 getAllSingers 并检查检索到的数组是否为空:

     func checkIfSingersAreEmpty() 
         let checkSingers = PersistenceController.shared.getAllSingers()
         if checkSingers.isEmpty 
             noMoreSingers = true
         
     
    

现在一切正常!

【讨论】:

以上是关于SwiftUI 列表在删除后继续显示核心数据项的主要内容,如果未能解决你的问题,请参考以下文章

如何从 SwiftUI 的详细信息视图(编辑项目视图)中删除核心数据条目?

在 macOS 上的 SwiftUI 列表视图中选择和删除核心数据实体

SwiftUI .swipeactions 和核心数据

浅谈CoreData海量数据(实时动态添加和删除)在SwiftUI列表中分页显示的原理及实现

如何从 SwiftUI 列表和领域中删除数据

SwiftUI - 如何限制 ForEach 中显示的对象数量