Ошибка NSFetchedResultsController: попытка удалить элемент из раздела 0, который содержит только 1 элемент, до обновления с помощью userInfo (null) - PullRequest
0 голосов
/ 06 декабря 2018

У меня есть приложение, которое использует Core Data.В нем есть посты, для каждого поста есть много тегов.Для каждого тега есть много постов.

У меня есть контроллер домашнего просмотра, который отображает коллекцию всех тегов.Источник данных для этого CollectionView работает от NSFetchedResultsController

class HomeViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource,NSFetchedResultsControllerDelegate {

    @IBOutlet weak var latestTagsCollectionView: UICollectionView!

    var fetchedResultsController: NSFetchedResultsController<Tag>!
    var blockOperations: [BlockOperation] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        self.latestTagsCollectionView.dataSource = self
        self.latestTagsCollectionView.delegate = self
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        //1
        guard let appDelegate =
            UIApplication.shared.delegate as? AppDelegate else {
                return
        }
        let managedContext =
            appDelegate.persistentContainer.viewContext
        //2
        let fetchRequest =
            NSFetchRequest<NSManagedObject>(entityName: "Tag")
        //3
        fetchRequest.sortDescriptors = []

        fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: managedContext, sectionNameKeyPath: nil, cacheName: nil) as? NSFetchedResultsController<Tag>
        fetchedResultsController.delegate = self

        do {
            try fetchedResultsController.performFetch()
        } catch let error as NSError {
            print("Could not fetch. \(error), \(error.userInfo)")
        }
    }

    func configure(cell: UICollectionViewCell, for indexPath: IndexPath) {

        guard let cell = cell as? TagCollectionViewCell else {
            return
        }
        print(indexPath,"indexPath")
        let tag = fetchedResultsController.object(at: indexPath)
        guard let timeAgo = tag.mostRecentUpdate as Date? else { return }
        cell.timeAgo.text = dateFormatter.string(from: timeAgo)
        if let imageData = tag.mostRecentThumbnail {
            cell.thumbnail.image = UIImage(data:imageData as Data,scale:1.0)
        } else {
            cell.thumbnail.image = nil
        }
        cell.tagName.text = tag.name
        cell.backgroundColor = UIColor.gray
    }

    //CollectionView Stuff
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        guard let sectionData = fetchedResultsController.sections?[section] else {
            return 0
        }
        return sectionData.numberOfObjects
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = latestTagsCollectionView.dequeueReusableCell(withReuseIdentifier: "latestTagCell", for: indexPath) as! TagCollectionViewCell
        configure(cell: cell, for: indexPath)
        return cell
    }

    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>,
                             didChange anObject: Any,
                             at indexPath: IndexPath?,
                             for type: NSFetchedResultsChangeType,
                             newIndexPath: IndexPath?){
        switch type {
        case .insert:
            print("Insert Object: \(String(describing: newIndexPath))")
            blockOperations.append(
                BlockOperation(block: { [weak self] in
                    if let this = self {
                        this.latestTagsCollectionView!.insertItems(at: [newIndexPath!])
                    }
                })
            )
        case .update:
            blockOperations.append(
                BlockOperation(block: { [weak self] in
                    if let this = self {
                        this.latestTagsCollectionView!.reloadItems(at: [newIndexPath!])
                    }
                })
            )
        case .move:
            blockOperations.append(
                BlockOperation(block: { [weak self] in
                    if let this = self {
                        this.latestTagsCollectionView!.moveItem(at: indexPath!, to: newIndexPath!)
                    }
                })
            )
        case .delete:
            print("deleted record")
            blockOperations.append(
                BlockOperation(block: { [weak self] in
                    if let this = self {
                        this.latestTagsCollectionView!.deleteItems(at: [newIndexPath!])
                    }
                })
            )
        default: break
        }
    }

    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        latestTagsCollectionView!.performBatchUpdates({ () -> Void in
            for operation: BlockOperation in self.blockOperations {
                operation.start()
            }
        }, completion: { (finished) -> Void in
            self.blockOperations.removeAll(keepingCapacity: false)
        })
    }

    deinit {
        // Cancel all block operations when VC deallocates
        for operation: BlockOperation in blockOperations {
            operation.cancel()
        }
        blockOperations.removeAll(keepingCapacity: false)
    }

На другом контроллере представления я разрешаю пользователям добавлять сообщения.Для каждого поста пользователь может добавить несколько разных тегов.Вот метод, который вызывается при сохранении сообщения:

func save(){
    guard let appDelegate =
    UIApplication.shared.delegate as? AppDelegate else {
        return
    }
    let managedContext =
        appDelegate.persistentContainer.viewContext
    var postTags:[Tag] = []

    if let tokens = tagsView.tokens() {
        for token in tokens {
            let tagFetchRequest: NSFetchRequest<Tag> = Tag.fetchRequest()
            tagFetchRequest.predicate = NSPredicate(format: "name == %@", token.title)
            do {
                let res = try managedContext.fetch(tagFetchRequest)
                var tag: Tag!
                if res.count > 0 {
                    tag = res.first
                } else {
                    tag = Tag(context: managedContext)
                    tag.name = token.title
                    tag.mostRecentUpdate = NSDate()
                    tag.mostRecentThumbnail = UIImage(named: "Plus")!.pngData() as! NSData
                }
                postTags.append(tag)
            } catch let error as NSError {
                print("Could not fetch. \(error), \(error.userInfo)")
                return
            }
        }
    }
    let post = Post(context: managedContext)
    for tag in postTags {
        tag.addToPosts(post)
        post.addToTags(tag)
    }
    post.mediaURI = URL(string: "https://via.placeholder.com/150")
    post.notes = "some notes..."
    post.timeStamp = Calendar.current.date(byAdding: .day, value: 8, to: Date()) as! NSDate
    do {
        try managedContext.save()
    } catch let error as NSError {
        print("Could not save. \(error), \(error.userInfo)")
    }
}

Как видите, каждый тег либо распознается в базе данных, либо создается, если он не существует.Для этой ошибки я начинаю без данных в базе данных.Сначала я создаю пост с тегом «один».Затем я возвращаюсь к представлению Home View Controller.Я вижу новый тег "One" создан.

Insert Object: Optional([0, 0])

печатается, так как регистр .insert применяется к методу обратного вызова NSFetchedResultsController.

Затем я добавляю сообщение с двумя тегами: «Один», «Два».

Insert Object: Optional([0, 0])
2018-12-05 12:51:16.947569-0800 SweatNetOffline[71327:19904799] *** Assertion failure in -[UICollectionView _endItemAnimationsWithInvalidationContext:tentativelyForReordering:animator:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKitCore_Sim/UIKit-3698.84.15/UICollectionView.m:5908
2018-12-05 12:51:16.949957-0800 SweatNetOffline[71327:19904799] [error] fault: Serious application error.  An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:.  attempt to delete item 1 from section 0 which only contains 1 items before the update with userInfo (null)

Почему здесь пытаются удалить элемент 1 ... Я не совсем понимаю это сообщение об ошибке.Я считаю, что нужно просто вставить элемент по новому пути индекса, поскольку в базе данных был создан новый тег «Два».Что здесь происходит?

1 Ответ

0 голосов
/ 06 декабря 2018

Ваша проблема вызвана использованием newIndexPath вместо indexPath в случае .update.

Когда вы присваиваете существующий тег сообщению, этот объект Tag обновляется.Это приводит к отправке события .updated в NSFetchResultsControllerDelegate.

В методе делегата newIndexPath представляет путь индекса объекта после обработки вставок , тогда как indexPath представляет путь индекса объекта до вставок.

Документация для performBatchUpdates:completion состояний:

Удаление обрабатываетсяперед вставками в пакетных операциях.Это означает, что индексы для удалений обрабатываются относительно индексов состояния представления коллекции перед пакетной операцией, а индексы для вставок обрабатываются относительно индексов состояния после всех удалений в пакетной операции.

Поскольку вставки выполняются в последний раз, при попытке перезагрузить newIndexPath вы получаете исключение, поскольку вы пытаетесь перезагрузить элемент, который еще не был вставлен.

Изменение кода для ссылки на indexPath в этом случае решит вашу проблему.

 case .update:
     blockOperations.append(
         BlockOperation(block: { [weak self] in
             if let this = self {
                 this.latestTagsCollectionView!.reloadItems(at: [indexPath!])
             }
         })
     )

Кроме того, вам нужно только обновить Post или Tag;поскольку существуют обратные ссылки, Core Data позаботится об обновлении других объектов

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...