在后台上下文中保存NSManagedObjects的问题

时间:2019-01-02 10:21:48

标签: ios swift multithreading core-data data-persistence

我一直为此苦苦挣扎。我将不胜感激。

我有一个位置NSManagedObject和一个图像NSManagedObject,它们具有一对多的关系,即一个位置有很多图像。

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

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

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

  1. 当我尝试保存从flickr检索的每个图像时,都会收到一条警告,指出存在一个悬空的对象,并且可以建立这种关系:

这是我的保存代码:

  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(在后台上下文中工作)检索数据时。集合视图有时会很好地查看图像,有时会出现此错误:

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中的更改更新“集合视图”有关。

  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()
        }
    }

我在做什么错?我将不胜感激。

4 个答案:

答案 0 :(得分:1)

我无法告诉您1)的问题是什么,但是我认为2)并不是(只是)数据库问题。

当您向集合视图添加或删除项目/节时,通常会遇到错误,但是随后调用 numberOfItemsInSection 时,这些数字不会累加。示例:您有5个项目并添加2,但是随后调用 numberOfItemsInSection 并返回6,这会导致不一致。

在您的情况下,我的猜测是您使用 collectionView.insertItems()添加项目,但此行此后返回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委托方法中访问数据库,而要有一组仅被提取一次并且仅在数据库内容更改时才更新的项目数组。这可能更高效,更容易维护。

答案 1 :(得分:1)

批处理更新期间,您经常遇到UICollectionView不一致的问题。 如果以不正确的顺序执行删除/添加新项目,UICollectionView可能会崩溃。 这个问题有2种典型的解决方案:

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

答案 2 :(得分:1)

问题是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

一旦数据通过以下链保存在 writeContext 中,

UICollectionView将正确更新:

  

writeContext(后台线程)-> readContext(主线程)-> NSFetchedResultsController(主线程)-> UICollectionView(主线程)

答案 3 :(得分:0)

我要感谢 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上创建一个对应的方法不起作用的想法与我所说的有所不同,请告诉我们。