在背景上下文中保存 NSManagedObjects 时出现问题

Problems saving NSManagedObjects on a background Context

我已经为此苦苦挣扎了好几天。我将不胜感激任何帮助。

我有一个位置 NSManagedObject 和一个图像 NSManagedObject,它们有 one-to-many 关系,即一个位置有很多图像。

我有 2 个屏幕,在第一个屏幕中,用户在视图上下文中添加位置,并且可以毫无问题地添加和检索它们。

现在,在第二个屏幕中,我想根据在第一个屏幕中选择的位置检索图像,然后在 Collection 视图中显示图像。图像首先从 flickr 中检索,然后保存在数据库中。

我想在背景上下文中保存和检索图像,这给我带来了很多问题。

  1. 当我尝试保存从 flickr 检索到的每张图像时,我收到一条警告,指出存在悬空 object 并且无法建立关系:

这是我的保存代码:

  func saveImagesToDb () {

        //Store the image in the DB along with its location on the background thread
        if (doesImageExist()){
            dataController.backgroundContext.perform {

                for downloadedImage in self.downloadedImages {
                    print ("saving to context")
                    let imageOnMainContext = Image (context: self.dataController.viewContext)
                    let imageManagedObjectId = imageOnMainContext.objectID
                    let imageOnBackgroundContext = self.dataController.backgroundContext.object(with: imageManagedObjectId) as! Image

                    let locationObjectId = self.imagesLocation.objectID
                    let locationOnBackgroundContext = self.dataController.backgroundContext.object(with: locationObjectId) as! Location

                    let imageData = NSData (data: downloadedImage.jpegData(compressionQuality: 0.5)!)
                    imageOnBackgroundContext.image = imageData as Data
                    imageOnBackgroundContext.location = locationOnBackgroundContext


                    try? self.dataController.backgroundContext.save ()
                }
            }
        }
    }

正如您在上面的代码中看到的,我正在基于从视图上下文中检索到的 ID 在后台上下文中构建 NSManagedObject。每次调用 saveImagesToDb 时我都会收到警告,这是什么问题?

  1. 尽管有上述警告,当我通过 FetchedResultsController(在后台上下文中工作)检索数据时。 Collection 视图有时可以很好地查看图像,有时我会收到此错误:

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of items in section 0. The number of items contained in an existing section after the update (4) must be equal to the number of items contained in that section before the update (1), plus or minus the number of items inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out).'

以下是一些与设置 FetchedResultsController 和根据上下文或 FetchedResultsController 中的更改更新 Collection 视图相关的代码片段。

  func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {

        guard let imagesCount = fetchedResultsController.fetchedObjects?.count else {return 0}

        return imagesCount
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        print ("cell data")
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "photoCell", for: indexPath) as! ImageCell
        //cell.placeImage.image = UIImage (named: "placeholder")

        let imageObject = fetchedResultsController.object(at: indexPath)
        let imageData = imageObject.image
        let uiImage = UIImage (data: imageData!)

        cell.placeImage.image = uiImage
        return cell
    }



func setUpFetchedResultsController () {
        print ("setting up controller")
        //Build a request for the Image ManagedObject
        let fetchRequest : NSFetchRequest <Image> = Image.fetchRequest()
        //Fetch the images only related to the images location

        let locationObjectId = self.imagesLocation.objectID
        let locationOnBackgroundContext = self.dataController.backgroundContext.object(with: locationObjectId) as! Location
        let predicate = NSPredicate (format: "location == %@", locationOnBackgroundContext)

        fetchRequest.predicate = predicate
        fetchRequest.sortDescriptors = [NSSortDescriptor(key: "location", ascending: true)]

        fetchedResultsController = NSFetchedResultsController (fetchRequest: fetchRequest, managedObjectContext: dataController.backgroundContext, sectionNameKeyPath: nil, cacheName: "\(latLongString) images")

        fetchedResultsController.delegate = self

        do {
            try fetchedResultsController.performFetch ()
        } catch {
            fatalError("couldn't retrive images for the selected location")
        }
    }

    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {

        print ("object info changed in fecthed controller")

        switch type {
        case .insert:
            print ("insert")
            DispatchQueue.main.async {
                print ("calling section items")
                self.collectionView!.numberOfItems(inSection: 0)
                self.collectionView.insertItems(at: [newIndexPath!])
            }
            break

        case .delete:
            print ("delete")

            DispatchQueue.main.async {
                self.collectionView!.numberOfItems(inSection: 0)
                self.collectionView.deleteItems(at: [indexPath!])
            }
            break
        case .update:
            print ("update")

            DispatchQueue.main.async {
                self.collectionView!.numberOfItems(inSection: 0)
                self.collectionView.reloadItems(at: [indexPath!])
            }
            break
        case .move:
            print ("move")

            DispatchQueue.main.async {
                self.collectionView!.numberOfItems(inSection: 0)
                self.collectionView.moveItem(at: indexPath!, to: newIndexPath!)

            }

        }
    }

    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
        print ("section info changed in fecthed controller")
        let indexSet = IndexSet(integer: sectionIndex)
        switch type {
        case .insert:
            self.collectionView!.numberOfItems(inSection: 0)
            collectionView.insertSections(indexSet)
            break
        case .delete:
            self.collectionView!.numberOfItems(inSection: 0)
            collectionView.deleteSections(indexSet)
        case .update, .move:
            fatalError("Invalid change type in controller(_:didChange:atSectionIndex:for:). Only .insert or .delete should be possible.")
        }

    }

    func addSaveNotificationObserver() {
        removeSaveNotificationObserver()
        print ("context onbserver notified")
        saveObserverToken = NotificationCenter.default.addObserver(forName: .NSManagedObjectContextObjectsDidChange, object: dataController?.backgroundContext, queue: nil, using: handleSaveNotification(notification:))
    }

    func removeSaveNotificationObserver() {
        if let token = saveObserverToken {
            NotificationCenter.default.removeObserver(token)
        }
    }

    func handleSaveNotification(notification:Notification) {
        DispatchQueue.main.async {
            self.collectionView!.numberOfItems(inSection: 0)
            self.collectionView.reloadData()
        }
    }

我做错了什么?我将不胜感激任何帮助。

我不能告诉你 1) 的问题是什么,但我认为 2) 不是(只是)数据库的问题。

您遇到的错误通常发生在您向集合视图添加或删除 items/sections 时,但是当随后调用 numberOfItemsInSection 时,数字不会相加。示例:您有 5 个项目并添加 2 个,但随后调用 numberOfItemsInSection 和 returns 6,这会造成不一致。

在你的情况下,我的猜测是你添加了带有 collectionView.insertItems() 的项目,但是此行 returns 0 之后:

guard let imagesCount = fetchedResultsController.fetchedObjects?.count else {return 0}

在你的代码中我也感到困惑的是这些部分:

 DispatchQueue.main.async {
            print ("calling section items")
            self.collectionView!.numberOfItems(inSection: 0)
            self.collectionView.insertItems(at: [newIndexPath!])
        }

您正在请求那里的项目数量,但您实际上并未对该函数的结果执行任何操作。这有什么原因吗?

即使我不知道 CoreData 的问题是什么,我还是建议您不要在 tableview 委托方法中访问数据库,而是要有一个项目数组,这些项目只获取一次并且仅在数据库出现时更新内容变化。这可能性能更高并且更容易维护。

您有一个常见问题,即在批量更新期间 UICollectionView 不一致。 如果您以错误的顺序执行 deletion/adding 个新项目 UICollectionView 可能会崩溃。 本题有2种典型解法:

  1. 使用 -reloadData() 而不是批量更新。
  2. 使用第三方库安全地执行批量更新。像这样 https://github.com/badoo/ios-collection-batch-updates

问题是 NSFetchedResultsController 应该只使用一个 主线程 NSManagedObjectContext。

解决方案:创建两个NSManagedObjectContext对象,一个在NSFetchedResultsController的主线程中,一个在后台线程中执行数据写入。

let writeContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) let readContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) let fetchedController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: readContext, sectionNameKeyPath: nil, cacheName: nil) writeContext.parent = readContext

UICollectionView 将在使用以下链将数据保存在 writeContext 中后正确更新:

writeContext(background thread ) -> readContext(main thread) -> NSFetchedResultsController (main thread) -> UICollectionView (main thread)

感谢 Robin Bork、Eugene El 和 meim 的回答。

我终于可以解决这两个问题了。

对于 CollectionView 的问题,我觉得我更新了太多次,正如你在代码中看到的那样,我曾经在两个 FetchedResultsController 委托方法中更新它,并且还通过一个观察者观察上下文的任何变化。所以我删除了所有这些并只使用了这个方法:

func controllerWillChangeContent(_ controller: 

    NSFetchedResultsController<NSFetchRequestResult>) {
            DispatchQueue.main.async {
                self.collectionView.reloadData()
            }
        }

除此之外,CollectionView 在维护一个部分中的项目计数方面存在一个错误,如 Eugene El 所述。因此,我只是使用 reloadData 来更新它的项目并且效果很好,我删除了任何逐项调整其项目的方法的使用,例如在特定 IndexPath.

处插入项目

对于悬挂物问题。从代码中可以看出,我有一个 Location 对象和一个 Image 对象。我的位置对象已经填充了一个位置,它来自 view context,所以我只需要使用它的 ID 从它创建一个相应的对象(如您在问题代码中看到的那样)。

问题出在图像对象上,我在 view context 上创建一个对象(其中没有插入数据),获取它的 ID,然后在 background context 上构建一个相应的对象。阅读此错误并思考我的代码后,我认为原因可能是 view context 上的 Image 对象不包含任何数据。因此,我删除了在 view context 上创建该对象的代码,并直接在 background context 上创建了一个对象,并在下面的代码中使用它,它成功了!

func saveImagesToDb () {

        //Store the image in the DB along with its location on the background thread
        dataController.backgroundContext.perform {
            for downloadedImage in self.downloadedImages {
                let imageOnBackgroundContext = Image (context: self.dataController.backgroundContext)

                //imagesLocation is on the view context
                let locationObjectId = self.imagesLocation.objectID
                let locationOnBackgroundContext = self.dataController.backgroundContext.object(with: locationObjectId) as! Location

                let imageData = NSData (data: downloadedImage.jpegData(compressionQuality: 0.5)!)
                imageOnBackgroundContext.image = imageData as Data
                imageOnBackgroundContext.location = locationOnBackgroundContext


                guard (try? self.dataController.backgroundContext.save ()) != nil else {
                    self.showAlert("Saving Error", "Couldn't store images in Database")
                    return
                }
            }
        }

    }

如果有人有与我所说的不同的想法,为什么第一个方法首先在 view context 上创建一个空的 Image 对象,然后在 background context 上创建一个相应的对象却没有工作,请告诉我们。