【问题标题】:NSFetchedResultsController fault: attempt to delete item from section 0 which only contains 1 items before the update with userInfo (null)NSFetchedResultsController 错误:尝试从第 0 节中删除项目,该项目在使用 userInfo (null) 更新之前仅包含 1 个项目
【发布时间】:2018-12-05 21:16:44
【问题描述】:

我有一个使用 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)")
    }
}

如您所见,每个标签要么在数据库中被识别,要么在不存在时被创建。对于这个错误,我从数据库中没有数据开始。首先,我创建一个带有“one”标签的帖子。然后我回到主视图控制器的视图。我看到创建了一个名为“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 项...我不太明白这个错误信息。我相信它应该只是在新的索引路径中插入一个项目,因为在数据库中创建了一个新的标签“Two”。这里发生了什么?

【问题讨论】:

  • 您的模型中是否设置了正确的反向关系?如果是这样,您应该将标签添加到帖子或将帖子添加到标签,但不能同时添加。
  • @Paulw11 Tag 实体与目标 Post 和 Inverse “标签”之间存在称为帖子的关系。 Post 实体具有称为标签的关系,带有目标标签和反向“帖子”。我读到这是你如何设置多对多关系,尽管逆向有点令人困惑。我如何知道是将标签添加到帖子还是将帖子添加到标签?在这种情况下,我正在创建一个带有这些标签的帖子,所以我假设只有帖子的标签......
  • 不管你用哪种方式,只做一种。我会在新帖子上设置标签
  • 任何一种方式都不能消除错误。该行为表明它可能会用新名称(“Two”)覆盖数据库中的原始标签。我意识到,如果我在第二篇文章中使用“Two”而不是“One”、“Two”,则不会出现此问题...
  • 我错了。删除数据库并进行更改确实可以解决此问题

标签: ios swift core-data nsfetchedresultscontroller


【解决方案1】:

您的问题是在 .update 案例中使用 newIndexPath 而不是 indexPath 引起的。

当您将现有标签分配给帖子时,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 将负责更新其他对象

【讨论】:

  • 你的意思是indexPath!而不是索引路径! ?此问题已解决,此更改以及您关于仅将标签添加到帖子而不将帖子添加到标签的建议。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-07-12
  • 1970-01-01
相关资源
最近更新 更多