代码之家  ›  专栏  ›  技术社区  ›  Dania

在后台上下文中保存nsmanagedObjects时出现问题

  •  0
  • Dania  · 技术社区  · 5 年前

    我已经为此挣扎了好几天了。我会感谢你的帮助。

    我有个地方 NSManagedObject 和一个图像 被管理对象 它们有一对多的关系,即一个位置有许多图像。

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

    现在,在第二个屏幕中,我希望根据在第一个屏幕中选择的位置检索图像,然后在集合视图中显示图像。图像首先从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 回复  |  直到 5 年前
        1
  •  1
  •   Robin Bork    5 年前

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

    当您向CollectionView添加或删除项/节时,通常会发生正在获取的错误,但当 分项工程数量 以后再打电话,号码不加起来。示例:您有5个项目并添加2个,但是 分项工程数量 调用并返回6,这会导致不一致。

    在你的情况下,我猜你会添加 集合视图.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委托方法中访问db,而是要有一个项目数组,该数组只在db内容更改时提取一次并更新。这可能更具性能,更易于维护。

        2
  •  1
  •   Eugene El    5 年前

    你有一个共同的问题 UICollectionView 批处理更新期间不一致。 如果按错误的顺序执行删除/添加新项目 ui集合视图 可能会崩溃。 这个问题有两个典型的解决方案:

    1. 使用-reloaddata()而不是批更新。
    2. 使用第三方库安全实现批更新。像这样的史密斯 https://github.com/badoo/ios-collection-batch-updates
        3
  •  1
  •   meim    5 年前

    问题是未保存的结果控制器应仅使用 主线 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(后台线程)->readContext(主线程)->nsfetchedResultsController(主线程)->uiCollectionView(主线程)

        4
  •  0
  •   Dania    5 年前

    我想谢谢你 Robin Bork、Eugene El和Meim 为了他们的答案。

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

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

    func controllerWillChangeContent(_ controller: 
    
        NSFetchedResultsController<NSFetchRequestResult>) {
                DispatchQueue.main.async {
                    self.collectionView.reloadData()
                }
            }
    

    除此之外,CollectionView在维护节中的项计数时也有一个错误,有时 尤金 提到。所以,我只是使用reloaddata来更新它的项,这很有效,我删除了任何一种方法的用法,这种方法一项一项地调整它的项,就像在特定的 IndexPath .

    对于悬挂物体的问题。从代码中可以看到,我有一个位置对象和一个图像对象。我的位置对象已经被一个位置填满,它来自 view context ,所以我只需要使用它的ID(如您在问题代码中看到的)从中创建一个对应的对象。

    问题出在图像对象中,我在 视图上下文 (不包含插入的数据),获取其ID,然后在 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
                    }
                }
            }
    
        }
    

    如果有人有不同于我所说的关于为什么第一个方法首先在 视图上下文 ,然后在 背景上下文 没用,请告诉我们。