NSFetchedResultsContollerDelegate для CollectionView
Я хотел бы использовать NSFetchedResultsControllerRelegate в CollectionViewController. Поэтому я просто изменил метод для TableViewController для CollectionView.
(void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
switch(type) {
case NSFetchedResultsChangeInsert:
[self.collectionView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]];
break;
case NSFetchedResultsChangeDelete:
[self.collectionView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] ];
break;
}
}
(void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {
UICollectionView *collectionView = self.collectionView;
switch(type) {
case NSFetchedResultsChangeInsert:
[collectionView insertItemsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]];
break;
case NSFetchedResultsChangeDelete:
[collectionView deleteItemsAtIndexPaths:[NSArray arrayWithObject:indexPath]];
break;
case NSFetchedResultsChangeUpdate:
[collectionView reloadItemsAtIndexPaths:[NSArray arrayWithObject:indexPath]];
break;
case NSFetchedResultsChangeMove:
[collectionView deleteItemsAtIndexPaths:[NSArray arrayWithObject:indexPath]];
[collectionView insertItemsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]];
break;
}
}
(void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.collectionView reloadData];
}
Но я не знаю, как справиться с WillChangeContent
(beginUpdates
за TableView
) а также DidChangeContent
(endUpdates
за TableVie
ж) для CollectionView
,
Все работает отлично, за исключением случаев, когда я перемещаю один элемент из одного раздела в другой раздел. Тогда я получаю следующую ошибку.
Обычно это ошибка в наблюдателе NSManagedObjectContextObjectsDidChangeNotification. Неверное обновление: недопустимое количество элементов в разделе 0....
Любая идея, как я могу решить эту проблему?
4 ответа
Объединить полученный контроллер результатов с представлением коллекции немного сложно. Проблема объясняется в
Если вы ищете, как обойти
NSInternalInconsistencyException
исключение во время выполнения сUICollectionView
У меня есть пример на GitHub, подробно описывающий, как ставить обновления в очередь из NSFetchedResultsControllerDelegate.Проблема в том, что существующие
UITableView
класс используетbeginUpdates
а такжеendUpdates
представить партии в виде таблицы.UICollectionView
есть новыйperformBatchUpdates:
метод, который принимает параметр блока для обновления представления коллекции. Это сексуально, но это не работает с существующей парадигмой для NSFetchedResultsController.
К счастью, эта статья также предоставляет пример реализации:
Из README:
Это пример того, как использовать новый
UICollectionView
сNSFetchedResultsController
, Хитрость заключается в том, чтобы ставить в очередь обновления, сделанные черезNSFetchedResultsControllerDelegate
пока контроллер не закончит свои обновления.UICollectionView
не имеет того жеbeginUpdates
а такжеendUpdates
тотUITableView
должен позволить ему легко работать сNSFetchedResultsController
, поэтому вы должны поставить их в очередь, или вы получите исключения во время выполнения внутренней согласованности.
Вот моя реализация со Swift. Сначала инициализируйте массив NSBlockOperations:
var blockOperations: [NSBlockOperation] = []
В контроллере изменится, заново инициализируй массив:
func controllerWillChangeContent(controller: NSFetchedResultsController) {
blockOperations.removeAll(keepCapacity: false)
}
В самом деле измените метод объекта:
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
if type == NSFetchedResultsChangeType.Insert {
println("Insert Object: \(newIndexPath)")
blockOperations.append(
NSBlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.insertItemsAtIndexPaths([newIndexPath!])
}
})
)
}
else if type == NSFetchedResultsChangeType.Update {
println("Update Object: \(indexPath)")
blockOperations.append(
NSBlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.reloadItemsAtIndexPaths([indexPath!])
}
})
)
}
else if type == NSFetchedResultsChangeType.Move {
println("Move Object: \(indexPath)")
blockOperations.append(
NSBlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.moveItemAtIndexPath(indexPath!, toIndexPath: newIndexPath!)
}
})
)
}
else if type == NSFetchedResultsChangeType.Delete {
println("Delete Object: \(indexPath)")
blockOperations.append(
NSBlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.deleteItemsAtIndexPaths([indexPath!])
}
})
)
}
}
В разделе "Изменил метод":
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
if type == NSFetchedResultsChangeType.Insert {
println("Insert Section: \(sectionIndex)")
blockOperations.append(
NSBlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.insertSections(NSIndexSet(index: sectionIndex))
}
})
)
}
else if type == NSFetchedResultsChangeType.Update {
println("Update Section: \(sectionIndex)")
blockOperations.append(
NSBlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.reloadSections(NSIndexSet(index: sectionIndex))
}
})
)
}
else if type == NSFetchedResultsChangeType.Delete {
println("Delete Section: \(sectionIndex)")
blockOperations.append(
NSBlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.deleteSections(NSIndexSet(index: sectionIndex))
}
})
)
}
}
И, наконец, в контроллере did сделал изменение метода содержания:
func controllerDidChangeContent(controller: NSFetchedResultsController) {
collectionView!.performBatchUpdates({ () -> Void in
for operation: NSBlockOperation in self.blockOperations {
operation.start()
}
}, completion: { (finished) -> Void in
self.blockOperations.removeAll(keepCapacity: false)
})
}
Я также добавил некоторый код в метод deinit, чтобы отменить операции, когда ViewController собирается освободиться:
deinit {
// Cancel all block operations when VC deallocates
for operation: NSBlockOperation in blockOperations {
operation.cancel()
}
blockOperations.removeAll(keepCapacity: false)
}
Я сделал решение @Plot своим собственным объектом и преобразовал его в Swift 2
import Foundation
import CoreData
class CollectionViewFetchedResultsControllerDelegate: NSObject, NSFetchedResultsControllerDelegate {
// MARK: Properties
private let collectionView: UICollectionView
private var blockOperations: [NSBlockOperation] = []
// MARK: Init
init(collectionView: UICollectionView) {
self.collectionView = collectionView
}
// MARK: Deinit
deinit {
blockOperations.forEach { $0.cancel() }
blockOperations.removeAll(keepCapacity: false)
}
// MARK: NSFetchedResultsControllerDelegate
func controllerWillChangeContent(controller: NSFetchedResultsController) {
blockOperations.removeAll(keepCapacity: false)
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case .Insert:
guard let newIndexPath = newIndexPath else { return }
let op = NSBlockOperation { [weak self] in self?.collectionView.insertItemsAtIndexPaths([newIndexPath]) }
blockOperations.append(op)
case .Update:
guard let newIndexPath = newIndexPath else { return }
let op = NSBlockOperation { [weak self] in self?.collectionView.reloadItemsAtIndexPaths([newIndexPath]) }
blockOperations.append(op)
case .Move:
guard let indexPath = indexPath else { return }
guard let newIndexPath = newIndexPath else { return }
let op = NSBlockOperation { [weak self] in self?.collectionView.moveItemAtIndexPath(indexPath, toIndexPath: newIndexPath) }
blockOperations.append(op)
case .Delete:
guard let indexPath = indexPath else { return }
let op = NSBlockOperation { [weak self] in self?.collectionView.deleteItemsAtIndexPaths([indexPath]) }
blockOperations.append(op)
}
}
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
switch type {
case .Insert:
let op = NSBlockOperation { [weak self] in self?.collectionView.insertSections(NSIndexSet(index: sectionIndex)) }
blockOperations.append(op)
case .Update:
let op = NSBlockOperation { [weak self] in self?.collectionView.reloadSections(NSIndexSet(index: sectionIndex)) }
blockOperations.append(op)
case .Delete:
let op = NSBlockOperation { [weak self] in self?.collectionView.deleteSections(NSIndexSet(index: sectionIndex)) }
blockOperations.append(op)
default: break
}
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
collectionView.performBatchUpdates({
self.blockOperations.forEach { $0.start() }
}, completion: { finished in
self.blockOperations.removeAll(keepCapacity: false)
})
}
}
Использование:
fetchedResultsController.delegate = CollectionViewFetchedResultsControllerDelegate(collectionView)
Версия Swift 4
private var blockOperations: [BlockOperation] = []
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
blockOperations.removeAll(keepingCapacity: false)
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>,
didChange anObject: Any,
at indexPath: IndexPath?,
for type: NSFetchedResultsChangeType,
newIndexPath: IndexPath?) {
let op: BlockOperation
switch type {
case .insert:
guard let newIndexPath = newIndexPath else { return }
op = BlockOperation { self.collectionView.insertItems(at: [newIndexPath]) }
case .delete:
guard let indexPath = indexPath else { return }
op = BlockOperation { self.collectionView.deleteItems(at: [indexPath]) }
case .move:
guard let indexPath = indexPath, let newIndexPath = newIndexPath else { return }
op = BlockOperation { self.collectionView.moveItem(at: indexPath, to: newIndexPath) }
case .update:
guard let indexPath = indexPath else { return }
op = BlockOperation { self.collectionView.reloadItems(at: [indexPath]) }
}
blockOperations.append(op)
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
collectionView.performBatchUpdates({
self.blockOperations.forEach { $0.start() }
}, completion: { finished in
self.blockOperations.removeAll(keepingCapacity: false)
})
}
Версия на 2020 год:
Основываясь на невероятных ответах выше,
Что соответствует знакомому примеру Apple для таблиц:
Рассмотрим знакомый пример Apple для представлений таблиц:
в заголовке
"Передача изменений данных в табличное представление" ...
Так,
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
insertRows(at: [newIndexPath!], with: .fade)
case .delete:
deleteRows(at: [indexPath!], with: .fade)
case .update:
reloadRows(at: [indexPath!], with: .fade)
case .move:
moveRow(at: indexPath!, to: newIndexPath!)
}
}
.
Вот "аналогичный шаблон" для копирования и вставки для представлений коллекции с текущим синтаксисом и т. Д.
var ops: [BlockOperation] = []
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
ops.append(BlockOperation(block: { [weak self] in
self?.insertItems(at: [newIndexPath!])
}))
case .delete:
ops.append(BlockOperation(block: { [weak self] in
self?.deleteItems(at: [indexPath!])
}))
case .update:
ops.append(BlockOperation(block: { [weak self] in
self?.reloadItems(at: [indexPath!])
}))
case .move:
ops.append(BlockOperation(block: { [weak self] in
self?.moveItem(at: indexPath!, to: newIndexPath!)
}))
@unknown default:
break
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
performBatchUpdates({ () -> Void in
for op: BlockOperation in self.ops { op.start() }
}, completion: { (finished) -> Void in self.ops.removeAll() })
}
deinit {
for o in ops { o.cancel() }
ops.removeAll()
}
.
(Я только что пропустил материал "секций", это то же самое.)
Ничего не делать в controllerWillChangeContent
?
В великолепном ответе @PhuahYeeKeat в controllerWillChangeContent
очищается массив ops. Я могу ошибаться, но для этого нет причин; он надежно опорожняется циклом пакетных обновлений. Просто ничего не делать вcontrollerWillChangeContent
.
Есть гонка?
Меня беспокоит, что произойдет, если новый didChange
прибывает в то время какperformBatchUpdates
обрабатывает предыдущий пакет.
Я правда не знаю performBatchUpdates
делает локальную копию или что-то еще - в этом случае глобальную копию следует удалить перед выполнением performBatchUpdates
?
ИДК.
Версия ответа на сюжет 2019 года:
var blockOperations: [BlockOperation] = []
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
blockOperations.removeAll(keepingCapacity: false)
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
if type == NSFetchedResultsChangeType.insert {
print("Insert Object: \(newIndexPath)")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.insertItems(at: [newIndexPath!])
}
})
)
}
else if type == NSFetchedResultsChangeType.update {
print("Update Object: \(indexPath)")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.reloadItems(at: [indexPath!])
}
})
)
}
else if type == NSFetchedResultsChangeType.move {
print("Move Object: \(indexPath)")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.moveItem(at: indexPath!, to: newIndexPath!)
}
})
)
}
else if type == NSFetchedResultsChangeType.delete {
print("Delete Object: \(indexPath)")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.deleteItems(at: [indexPath!])
}
})
)
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
if type == NSFetchedResultsChangeType.insert {
print("Insert Section: \(sectionIndex)")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.insertSections(IndexSet(integer: sectionIndex))
}
})
)
}
else if type == NSFetchedResultsChangeType.update {
print("Update Section: \(sectionIndex)")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.reloadSections(IndexSet(integer: sectionIndex))
}
})
)
}
else if type == NSFetchedResultsChangeType.delete {
print("Delete Section: \(sectionIndex)")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.deleteSections(IndexSet(integer: sectionIndex))
}
})
)
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
collectionView!.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)
}
Вот немного Swift, который работает с installsStandardGestureForInteractiveMovement из UICollectionViewController, немного засушен и включает installsStandardGestureForInteractiveMovement, так что все пути кода очевидны. Это тот же общий шаблон, что и код Plot.
var fetchedResultsProcessingOperations: [NSBlockOperation] = []
private func addFetchedResultsProcessingBlock(processingBlock:(Void)->Void) {
fetchedResultsProcessingOperations.append(NSBlockOperation(block: processingBlock))
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case .Insert:
addFetchedResultsProcessingBlock {self.collectionView!.insertItemsAtIndexPaths([newIndexPath!])}
case .Update:
addFetchedResultsProcessingBlock {self.collectionView!.reloadItemsAtIndexPaths([indexPath!])}
case .Move:
addFetchedResultsProcessingBlock {
// If installsStandardGestureForInteractiveMovement is on
// the UICollectionViewController will handle this on its own.
guard !self.installsStandardGestureForInteractiveMovement else {
return
}
self.collectionView!.moveItemAtIndexPath(indexPath!, toIndexPath: newIndexPath!)
}
case .Delete:
addFetchedResultsProcessingBlock {self.collectionView!.deleteItemsAtIndexPaths([indexPath!])}
}
}
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
switch type {
case .Insert:
addFetchedResultsProcessingBlock {self.collectionView!.insertSections(NSIndexSet(index: sectionIndex))}
case .Update:
addFetchedResultsProcessingBlock {self.collectionView!.reloadSections(NSIndexSet(index: sectionIndex))}
case .Delete:
addFetchedResultsProcessingBlock {self.collectionView!.deleteSections(NSIndexSet(index: sectionIndex))}
case .Move:
// Not something I'm worrying about right now.
break
}
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
collectionView!.performBatchUpdates({ () -> Void in
for operation in self.fetchedResultsProcessingOperations {
operation.start()
}
}, completion: { (finished) -> Void in
self.fetchedResultsProcessingOperations.removeAll(keepCapacity: false)
})
}
deinit {
for operation in fetchedResultsProcessingOperations {
operation.cancel()
}
fetchedResultsProcessingOperations.removeAll()
}