为啥在 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
添加一行Student
,frc
将触发所需的方法以在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 错误? (用于更改跟踪)