Использование UICollectionView с CoreData и NSFetchedResultsController

Недавно я начал другой проект, немного исследуя 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 я пытаюсь вызвать numberOfObjects в разделе 0 и получаю следующую ошибку

*** 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)

Заранее спасибо за то, что прочитали эту длинную статью и помогли мне!




Ответы (2)


Похоже, проблема у меня была некоторое время назад, когда я использовал объект C.

Проверить

Ошибка утверждения UICollectionView

Мне пришлось поработать, чтобы заставить мой FRC работать с представлением коллекции.

Похоже, вы нашли тот же мерзавец, что и я, вот исправление, которое решает проблему с представлением коллекции.

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

person DogCoffee    schedule 19.06.2015
comment
Ссылка Git была удалена. Всегда публикуйте необходимый фрагмент. Я голосую против этого ответа, так как сейчас он бесполезен. - person Master AgentX; 26.05.2020

Вот моя идея реализации методов делегата FRC.

В этом случае для подкласса UICollectionViewController:

Swift 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()
        })
    }

}
person Alex Shubin    schedule 23.06.2017
comment
При попытке обработать несколько обновлений в одно и то же время это может вызвать утверждение. Мне пришлось переместить self.sectionChanges.removeAll() self.itemChanges.removeAll() в основной блок performBatchUpdates, чтобы избежать этих проблем. - person robhasacamera; 06.07.2018
comment
Обязательно ли использовать метод controllerWillChangeContent для очистки списка 2 изменений? - person bubuxu; 21.02.2019