Проблемы с сохранением NSManagedObjects в фоновом контексте - PullRequest
0 голосов
/ 02 января 2019

Я боролся с этим уже несколько дней.Я буду признателен за любую помощь.

У меня есть Местоположение 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 ()
                }
            }
        }
    }

Как вы можете видеть из кода выше, я строю NSManagedObject на фоне контекста, основываясь на идентификаторе, полученном из тех, что в контексте представления.Каждый раз, когда вызывается saveImagesToDb, я получаю предупреждение, так в чем же проблема?

Несмотря на предупреждение выше, когда я получаю данные через 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 голосов
/ 03 января 2019

Я хотел бы поблагодарить Робин Борк, Юджина Эла и Мейм за их ответы.

Я мог бы наконец решить обе проблемы.

Для проблемы CollectionView я чувствовал, что обновлял ее слишком много раз, как вы можете видеть в коде, я использовал ее для обновления двумя FetchedResultsController методами делегатов, а также через наблюдателя, который наблюдаетлюбые изменения в контексте.Поэтому я удалил все это и просто использовал этот метод:

func controllerWillChangeContent(_ controller: 

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

В дополнение к этому, в CollectionView есть ошибка в поддержании количества элементов в разделе, иногда упоминаемом Eugene El .Итак, я просто использовал reloadData для обновления своих элементов, и это работало хорошо, я удалил использование любого метода, который корректирует его элементы по элементам, например, вставил элемент в определенный IndexPath.

Для висящего объектапроблема.Как видно из кода, у меня был объект Location и объект Image.Мой объект местоположения уже был заполнен местоположением, и он исходил из view context, поэтому мне просто нужно было создать соответствующий объект из него, используя его идентификатор (как вы видите в коде в вопросе).

Проблема была в объекте изображения, я создавал объект на view context (который не содержит никаких вставленных данных), получал его идентификатор, затем строил соответствующий объект на background context.Прочитав об этой ошибке и подумав о своем коде, я подумал, что причина может быть в том, что объект Image в view context не содержит никаких данных.Итак, я удалил код, который создает этот объект на 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
                }
            }
        }

    }

Если у кого-то естьдругая мысль, отличная от той, что я говорил о том, почему первый метод, который сначала создает пустой объект Image в view context, а затем создает соответствующий объект в background context, не работает, пожалуйста, сообщите нам об этом.

0 голосов
/ 02 января 2019

У вас общая проблема с несоответствием UICollectionView во время пакетного обновления.При выполнении удаления / добавления новых элементов в неправильном порядке UICollectionView может произойти сбой.Эта проблема имеет 2 типичных решения:

  1. использование -reloadData () вместо пакетных обновлений.
  2. использование сторонних библиотек с безопасной реализацией пакетного обновления.Что-то вроде этого https://github.com/badoo/ios-collection-batch-updates
0 голосов
/ 03 января 2019

Проблема в том, что 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 (фоновый поток) -> readContext (основной поток) -> NSFetchedResultsController (основной поток) -> UICollectionView (основной поток)

0 голосов
/ 02 января 2019

Я не могу сказать вам, в чем проблема с 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, а иметь массив элементов, который выбирается один раз и обновляется только при изменении содержимого базы данных. , Это, вероятно, более производительно и намного проще в обслуживании.

...