将 UICollectionView 与 CoreData 和 NSFetchedResultsController 一起使用

Posted

技术标签:

【中文标题】将 UICollectionView 与 CoreData 和 NSFetchedResultsController 一起使用【英文标题】:Using UICollectionView with CoreData and NSFetchedResultsController 【发布时间】:2015-06-19 07:59:43 【问题描述】:

我最近开始了另一个项目,稍微探索一下 Swift。我想使用 NSFetchedResultsController 实现一个集合视图,以从我的 CoreData 数据库中获取数据。我想使用来自https://github.com/AshFurrow/UICollectionView-NSFetchedResultsController 的示例 并在 Swift 中实现类似的东西。我不需要任何移动事件,所以我实现了以下内容:

首先我创建了一个类来保存所做的更改:

class ChangeItem
    var index:NSIndexPath
    var type:NSFetchedResultsChangeType

    init(index: NSIndexPath, type: NSFetchedResultsChangeType)
        self.index = index
        self.type = type
    

在我的集合视图控制器中,我使用两个数组来临时保存更改

var sectionChanges:[ChangeItem] = []
var objectChanges:[ChangeItem] = []

然后我等待我的 FetchedResultsController 在对 CoreData 数据库进行更改后进行更改

func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?,forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) 
    var item:ChangeItem?
    if(type == NSFetchedResultsChangeType.Insert)
        item = ChangeItem(index: newIndexPath!, type: type)
    else if(type == NSFetchedResultsChangeType.Delete)
        item = ChangeItem(index: indexPath!, type: type)
    else if(type == NSFetchedResultsChangeType.Update)
        item = ChangeItem(index: indexPath!, type: type)
    
    if(item != nil)
        self.objectChanges.append(item!)
    


func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) 
    var item:ChangeItem?


    if(type == NSFetchedResultsChangeType.Insert || type == NSFetchedResultsChangeType.Delete || type == NSFetchedResultsChangeType.Update)
        item = ChangeItem(index: NSIndexPath(forRow: 0, inSection: sectionIndex), type: type)
    
    if(item != nil)
        self.sectionChanges.append(item!)
    

应用所有更改后,FetchedResultsController 将执行 didChangeContent 方法

func controllerDidChangeContent(controller: NSFetchedResultsController) 
    println("something happened")

    self.collectionView!.performBatchUpdates( () -> Void in
        while(self.sectionChanges.count > 0 || self.objectChanges.count > 0)
            self.insertSections()
            self.insertItems()
            self.updateItems()
            let delips = self.deleteSections()
            self.deleteItems(delips)
        

        , completion:  (done) -> Void in
            if(done)
                println("done")
                self.collectionView.reloadData()
            else
                println("not done")
                self.collectionView.reloadData()
            
            if(self.fetchedResultsController.sections != nil && self.fetchedResultsController.sections?.count > 0)
                println("number of items in first section \(self.fetchedResultsController.sections![0].count)")
            
    )


    self.deselectAll()
    self.collectionView.reloadData()
    if(self.fetchedResultsController.sections != nil && self.fetchedResultsController.sections!.count > 0)
        for  i in 1 ..< self.fetchedResultsController.sections!.count
            if(self.fetchedResultsController.sections!.count > 0)
                println(self.fetchedResultsController.sections![i].numberOfObjects )
            
        
    


然后再次调用以下方法

private func deleteSections()->NSIndexSet
    var deletes = self.sectionChanges.filter( (item) -> Bool in
        if(item.type == NSFetchedResultsChangeType.Delete)

            let index = (self.sectionChanges as NSArray).indexOfObject(item)
            self.sectionChanges.removeAtIndex(index)
            return true
        


        return false
    ) as [ChangeItem]
    var indexSet:NSMutableIndexSet = NSMutableIndexSet()
    for del in deletes

        indexSet.addIndex(del.index.section)
    
    if(indexSet.count > 0 && self.collectionView.numberOfSections() > 0)
        self.collectionView.deleteSections(indexSet)
    
    return indexSet


private func insertSections()
    var inserts = self.sectionChanges.filter( (item) -> Bool in
        if(item.type == NSFetchedResultsChangeType.Insert)
            let index = (self.sectionChanges as NSArray).indexOfObject(item)
            self.sectionChanges.removeAtIndex(index)
            return true
        
        return false
    ) as [ChangeItem]
    var indexSet:NSMutableIndexSet = NSMutableIndexSet()
    for ins in inserts
        indexSet.addIndex(ins.index.section)
    
    if(indexSet.count > 0)
        println("Adding \(indexSet.count) section")
        println(indexSet)

        self.collectionView.insertSections(indexSet)
        self.collectionView.reloadSections(indexSet)
    


private func deleteItems(deletedSections:NSIndexSet?)
    var deletes = self.objectChanges.filter( (item) -> Bool in
        if(item.type == NSFetchedResultsChangeType.Delete)
            let index = (self.objectChanges as NSArray).indexOfObject(item)
            self.objectChanges.removeAtIndex(index)
            return true
        
    return false
    ) as [ChangeItem]
    var indexPaths:[NSIndexPath] = []

    for del in deletes
        if(del.index.section < self.fetchedResultsController.sections?.count)

            if(deletedSections == nil || deletedSections!.containsIndex(del.index.section))
                indexPaths.append(del.index)
            
        
    
    /*indexPaths = indexPaths.sorted( (a, b) -> Bool in
        if(a.section >= b.section && a.row >= b.row)
            return true
        
        return false
    )
    println(indexPaths)*/
    //self.collectionView.numberOfItemsInSection(0)
    if(indexPaths.count > 0)
        println("deleting \(indexPaths.count) items")
        self.collectionView.deleteItemsAtIndexPaths(indexPaths)
    



private func updateItems()

    var updates = self.objectChanges.filter( (item) -> Bool in
        if(item.type == NSFetchedResultsChangeType.Update)
            let index = (self.objectChanges as NSArray).indexOfObject(item)
            self.objectChanges.removeAtIndex(index)
            return true
        
        return false
    ) as [ChangeItem]
    var indexPaths:[NSIndexPath] = []
    for update in updates
        indexPaths.append(update.index)
    
    if(indexPaths.count > 0 )
        println("did update on \(indexPaths.count) items")
        self.collectionView.reloadItemsAtIndexPaths(indexPaths)
    


private func insertItems()
    var inserts = self.objectChanges.filter( (item) -> Bool in
        if(item.type == NSFetchedResultsChangeType.Insert)
            let index = (self.objectChanges as NSArray).indexOfObject(item)
            self.objectChanges.removeAtIndex(index)
            return true
        
        return false
    ) as [ChangeItem]
    var indexPaths:[NSIndexPath] = []
    for ins in inserts
        indexPaths.append(ins.index)
    
    if(indexPaths.count > 0 && self.numberOfSectionsInCollectionView(self.collectionView) > 0)

        println("Adding \(indexPaths.count) items to collection view with \(self.collectionView.numberOfItemsInSection(0))")
        self.collectionView.insertItemsAtIndexPaths(indexPaths)
        println("Did add items")
    


一次删除多个项目和部分工作正常(现在),但插入一个部分不起作用。如您所见,我在 insertSections 方法中实现了一个输出。我的测试场景中的新部分的数量是 1,我得到了正确的 NSIndexSet 和 1 个项目:(0)。但是在 insertItems 方法中,我尝试在第 0 部分调用 numberOfObjects 并得到以下错误

*** Assertion failure in -[UICollectionViewData numberOfItemsInSection:], /SourceCache/UIKit_Sim/UIKit3347.44/UICollectionViewData.m:5942015-06-19:00:53:01.966 Grocli[42533:1547866] CoreData: error: Serious application error.  An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:.  request for number of items in section 0 when there are only 0 sections in the collection view with userInfo (null)

提前感谢您阅读那篇长文并帮助我!

【问题讨论】:

【参考方案1】:

这是我实现 FRC 委托方法的想法。

在这种情况下为 UICollectionViewController 子类:

斯威夫特 3

import UIKit
import CoreData

class FetchedResultsCollectionViewController: UICollectionViewController, NSFetchedResultsControllerDelegate 

    private var sectionChanges = [(type: NSFetchedResultsChangeType, sectionIndex: Int)]()
    private var itemChanges = [(type: NSFetchedResultsChangeType, indexPath: IndexPath?, newIndexPath: IndexPath?)]()

    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) 
        sectionChanges.append((type, sectionIndex))
    

    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?)
    
        itemChanges.append((type, indexPath, newIndexPath))
    

    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>)
    
        collectionView?.performBatchUpdates(

            for change in self.sectionChanges 
                switch change.type 
                case .insert: self.collectionView?.insertSections([change.sectionIndex])
                case .delete: self.collectionView?.deleteSections([change.sectionIndex])
                default: break
                
            

            for change in self.itemChanges 
                switch change.type 
                case .insert: self.collectionView?.insertItems(at: [change.newIndexPath!])
                case .delete: self.collectionView?.deleteItems(at: [change.indexPath!])
                case .update: self.collectionView?.reloadItems(at: [change.indexPath!])
                case .move:
                    self.collectionView?.deleteItems(at: [change.indexPath!])
                    self.collectionView?.insertItems(at: [change.newIndexPath!])
                
            

        , completion:  finished in
            self.sectionChanges.removeAll()
            self.itemChanges.removeAll()
        )
    


【讨论】:

当试图同时处理多个更新时,这可能会导致发生断言。我不得不将self.sectionChanges.removeAll() self.itemChanges.removeAll() 移动到主 performBatchUpdates 块中以避免这些问题。 是否需要同时使用controllerWillChangeContent方法清空2个变更列表?【参考方案2】:

听起来像是我不久前在使用客观 C 时遇到的问题。

退房

UICollectionView Assertion failure

我必须做一些工作才能让我的 FRC 与集合视图一起使用。

好像你找到了和我一样的 git,这里是解决集合视图问题的修复程序。

https://github.com/AshFurrow/UICollectionView-NSFetchedResultsController/issues/13

【讨论】:

Git 链接已被删除。总是发布一些必需的sn-p。我不赞成这个答案,因为这现在没用了。

以上是关于将 UICollectionView 与 CoreData 和 NSFetchedResultsController 一起使用的主要内容,如果未能解决你的问题,请参考以下文章

动画前在 UICollectionView 中使用 Core Data 加载数据

如何将 Plist 文件与 UITableView / UICollectionView 一起使用?

将 UICollectionView 与 CoreData 和 NSFetchedResultsController 一起使用

UICollectionView 水平滚动与静态焦点项目

将 UICollectionView 添加到 UIView

检测用户将项目拖出 UICollectionView?