为啥在 Core Data container.performBackgroundTask() 之后不触发 tableView.reloadData()

Posted

技术标签:

【中文标题】为啥在 Core Data container.performBackgroundTask() 之后不触发 tableView.reloadData()【英文标题】:Why tableView.reloadData() is not triggered after Core Data container.performBackgroundTask()为什么在 Core Data container.performBackgroundTask() 之后不触发 tableView.reloadData() 【发布时间】:2018-04-08 06:02:04 【问题描述】:

我正在使用 Swift 4 构建具有 UITableViewController 的单视图 ios 11 应用程序,该应用程序也定义为 delegate 用于 NSFetchedResultsController

class MyTVC: UITableViewController, NSFetchedResultsControllerDeleagate 
    var container:NSPersistentContainer? = 
           (UIApplication.shared.delegate as? AppDelegate)?.persistentContainer

   var frc : NSFetchedResultsController<Student>? 

   override func viewDidLoad() 
      container?.performBackgroundTask  context in 
          // adds 100 dummy records in background
          for i in 1...100 
             let student = Student(context: context)
             student.name = "student \(i)"
          
          try? context.save()   // this works because count is printed below
          if let count = try? context.count(for: Student.fetchRequest()) 
               print("Number of students in core data: \(count)")  // prints 100
          
        // end of background inserting.

      // now defining frc:
      if let context = container?.viewContext 
          let request:NSFetchRequest<Student> = Student.fetchRequest()
          request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
          frc = NSFetchedResultsController<Student> (
               fetchRequest: request,
               managedObjectContext: context,
               sectionNameKeyPath: nil,
               cacheName: nil )

          try? frc?.performFetch()   // this works and I get no errors
          tableView.reloadData()
          frc.delegate = self
        // end of frc definition
   

如果我使用viewContext 添加一行Studentfrc 将触发所需的方法以在tableView 中显示它。然而,100 个虚拟行未显示。事实上,如果我试图在插入完成后告诉 tableview 重新加载,我的应用程序开始表现得很奇怪并变得有问题,并且没有做它应该做的事情(即:不删除行,不编辑等) .

但是如果我重新启动我的应用程序,而不调用虚拟插入,我可以看到前一次运行插入的 100 行。

唯一的问题是我无法从后台线程调用tableView.reloadData(),所以我尝试这样做:

// after printing the count, I did this:
DispatchQueue.main.async  [weak self] in
    self?.tableView.reloadData()   // causes UI to behave weirdly 

然后我尝试调用viewContext.perform 在正确的线程中重新加载表格视图

func viewDidLoad() 

   // code for inserting 100 dummy rows in background thread

   // code for defining frc and setting self as delegate

   if let context = container?.viewContext 
      context.perform  [weak self] in
         self?.tableView.reloadData()    // that also causes UI to behave weirdly

      
   

如何告诉我的 tableview 以线程安全的方式重新加载和显示 100 个虚拟行?

【问题讨论】:

在执行获取之前尝试设置委托。 @JonRose 我试过了。如果我在执行调用之后设置委托,则委托方法不会触发。所以,刷新不会发生,应用程序更容易出错 @Ahmad “导致 UI 行为怪异”是什么意思? @staticVoidMan 动画变得迟缓并且表格行上的删除按钮不会触发委托方法commiteditingstyle @Ahmad 您可以创建一个调试存储库并将其添加到您的问题中吗?我认为如果我们可以在调试 repo 上为您提供帮助会更快。 【参考方案1】:
override func viewDidLoad() 
    super.viewDidLoad()

    //Always need your delegate for the UI to be set before calling the UI's delegate functions.
    frc.delegate = self

    //First we can grab any already stored values.
    goFetch()

    //This chunk just saves. I would consider putting it into a separate function such as "goSave()" and then call that from an event handler.
    container?.performBackgroundTask  context in
        //We are in a different queue than the main queue, hence "backgroundTask".
        for i in 1...100 
            let student = Student(context: context)
            student.name = "student \(i)"
        
        try? context.save()   // this works because count is printed below
        if let count = try? context.count(for: Student.fetchRequest()) 
            print("Number of students in core data: \(count)")  // prints 100
        
        //Now that we are done saving its ok to fetch again.

        goFetch()

    

    //goFetch(); Your other code was running here would start executing before the backgroundTask is done. bad idea.
    //The reason it works if you restart the app because that data you didn't let finish saving is persisted
    //So the second time Even though its saving another 100 in another queue there were still at least 100 records to fetch at time of fetch.




func goFetch() 

    if let context = container?.viewContext 
        let request:NSFetchRequest<Student> = Student.fetchRequest()
        request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
        frc = NSFetchedResultsController<Student> (
            fetchRequest: request,
            managedObjectContext: context,
            sectionNameKeyPath: nil,
            cacheName: nil )

        try? frc?.performFetch()

        //Now that records are both stored and fetched its safe for our delegate to access the data on the main thread.
        //To me it would make sense to do a tableView reload everytime data is fetched so I placed this inside o `goFetch()`
        DispatchQueue.main.async  [weak self] in
            self?.tableView.reloadData()
        
    

【讨论】:

【参考方案2】:

在大量阅读有关 NSFetchedResultsController 和 NSPersistentContainer 并最终在 SO 找到重要信息后,我想我有一个可行的示例。

我的代码略有不同,因为我为此使用了一个项目。无论如何,这就是我所做的:

在我的视图控制器中,我的容器有一个属性

private var persistentContainer = NSPersistentContainer(name: coreDataModelName)

在 viewDidLoad 中,我加载了持久存储并创建了 100 条记录。

persistentContainer.loadPersistentStores  persistentStoreDescription, error in
  if let error = error 
    print("Unable to add Persistent Store [\(error)][\(error.localizedDescription)]")
   else 
    self.createFakeNotes() // Here 100 elements get created
    DispatchQueue.main.async 
      self.setupView() // other stuff, not relevant
      self.fetchNotes() // fetch using fetch result controller
      self.tableView.reloadData()
    
  

下面是 createFakeNotes(),我在其中使用单独的上下文在后台线程中插入元素,这段代码几乎取自 Apple's Core Data programming guide,但为了更新 UI,我需要将自动MergesChangesFromParent 设置为 true,我发现出this SO answer

为了方便测试,我也先删除旧笔记。

private func createFakeNotes() 
  let deleteRequest = NSBatchDeleteRequest(fetchRequest: Note.fetchRequest())
  do 
    try persistentContainer.persistentStoreCoordinator.execute(deleteRequest, with: persistentContainer.viewContext)
   catch 
    print("Delete error [\(error)]")
    return
  

  let privateContext = persistentContainer.newBackgroundContext()
  privateContext.automaticallyMergesChangesFromParent = true //Important!!!

  privateContext.perform 
    let createDate = Date()
    for i in 1...100 
      let note = Note(context: privateContext)
      note.title = String(format: "Title %2d", i)
      note.contents = "Content"
      note.createdAt = createDate
      note.updatedAt = createDate
    
    do 
      try privateContext.save()
      do 
        try self.persistentContainer.viewContext.save()
       catch 
        print("Fail saving main context [\(error.localizedDescription)")
      
     catch 
      print("Fail saving private context [\(error.localizedDescription)")
  

【讨论】:

嗨@joakim,我使用了上述方法,但它使应用程序崩溃。我在收到响应时同时插入和获取数据我的应用程序性能很慢。我们做什么? Joakim,感谢您提供有关 automaticMergesChangesFromParent 的精彩提示【参考方案3】:

您应该通过从 viewwillappear 调用数据来获取数据,然后尝试重新加载您的 tableview。

override func viewWillAppear(_ animated: Bool) 
    getdata()
    tableView.reloadData()

 func getdata() 
    let context =  (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
    do
    persons = try context.fetch(Person.fetchRequest()) 
    
    catch 
        print("fetching failed")
    

【讨论】:

以上是关于为啥在 Core Data container.performBackgroundTask() 之后不触发 tableView.reloadData()的主要内容,如果未能解决你的问题,请参考以下文章

为啥实体框架 System.Data.Entity.Core.Objects.RelationshipEntry 错误? (用于更改跟踪)

Core Data:为啥要创建自定义持久存储?

为啥我存储在 Core Data 中的布尔值没有立即显示?

为啥 Core Data 上下文对象必须通过环境变量传递?

为啥我可以在 viewDidAppear 中访问 Core Data 实体,但不能在 viewDidLoad 中访问?

为啥 Core Data 插入需要越来越长的时间?