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

NSFetchedResultsController错误:尝试从节0中删除项,该节在使用userInfo(null)进行更新之前仅包含1项

  •  0
  • BigBoy1337  · 技术社区  · 6 年前

    我有一个使用核心数据的应用程序。里面有帖子,每个帖子都有很多标签。每个标签都有许多帖子。

    我有一个主视图控制器,它显示所有标记的集合视图。此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)")
        }
    }
    

    正如您所见,每个标记要么在数据库中被识别,要么在不存在时被创建。对于这个bug,我从数据库中没有数据开始。首先,我创建了一个标签为“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”。这是怎么回事?

    1 回复  |  直到 6 年前
        1
  •  1
  •   Paulw11    6 年前

    您的问题是由使用 newIndexPath 而不是 indexPath .update 案例

    将现有标记指定给帖子时 Tag 对象已更新。这导致了一场灾难 .updated 要发布到 NSFetchResultsControllerDelegate

    在委托方法中 新索引XPath 之后 插入已处理,而 索引 插入。

    文件 performBatchUpdates:completion

    在批处理操作中,删除是在插入之前处理的。这意味着在批处理操作之前,删除的索引相对于集合视图状态的索引进行处理,而插入的索引相对于批处理操作中所有删除之后的状态索引进行处理。

    因为插入是最后一次执行的,所以当您尝试重新加载时 新索引XPath

    更改代码以引用 在这种情况下,可以解决您的问题。

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

    此外,您只需要更新 Post 标签 ; 由于存在反向引用,核心数据将负责更新其他对象