Skip to content

Instantly share code, notes, and snippets.

@iwasrobbed
Last active June 5, 2020 20:34
Show Gist options
  • Star 81 You must be signed in to star a gist
  • Fork 10 You must be signed in to fork a gist
  • Save iwasrobbed/5528897 to your computer and use it in GitHub Desktop.
Save iwasrobbed/5528897 to your computer and use it in GitHub Desktop.
UICollectionView w/ NSFetchedResultsController & NSBlockOperation. Idea originated from Blake Watters (https://github.com/AshFurrow/UICollectionView-NSFetchedResultsController/issues/13)
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
self.shouldReloadCollectionView = NO;
self.blockOperation = [[NSBlockOperation alloc] init];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id<NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
__weak UICollectionView *collectionView = self.collectionView;
switch (type) {
case NSFetchedResultsChangeInsert: {
[self.blockOperation addExecutionBlock:^{
[collectionView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]];
}];
break;
}
case NSFetchedResultsChangeDelete: {
[self.blockOperation addExecutionBlock:^{
[collectionView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]];
}];
break;
}
case NSFetchedResultsChangeUpdate: {
[self.blockOperation addExecutionBlock:^{
[collectionView reloadSections:[NSIndexSet indexSetWithIndex:sectionIndex]];
}];
break;
}
default:
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
__weak UICollectionView *collectionView = self.collectionView;
switch (type) {
case NSFetchedResultsChangeInsert: {
if ([self.collectionView numberOfSections] > 0) {
if ([self.collectionView numberOfItemsInSection:indexPath.section] == 0) {
self.shouldReloadCollectionView = YES;
} else {
[self.blockOperation addExecutionBlock:^{
[collectionView insertItemsAtIndexPaths:@[newIndexPath]];
}];
}
} else {
self.shouldReloadCollectionView = YES;
}
break;
}
case NSFetchedResultsChangeDelete: {
if ([self.collectionView numberOfItemsInSection:indexPath.section] == 1) {
self.shouldReloadCollectionView = YES;
} else {
[self.blockOperation addExecutionBlock:^{
[collectionView deleteItemsAtIndexPaths:@[indexPath]];
}];
}
break;
}
case NSFetchedResultsChangeUpdate: {
[self.blockOperation addExecutionBlock:^{
[collectionView reloadItemsAtIndexPaths:@[indexPath]];
}];
break;
}
case NSFetchedResultsChangeMove: {
[self.blockOperation addExecutionBlock:^{
[collectionView moveItemAtIndexPath:indexPath toIndexPath:newIndexPath];
}];
break;
}
default:
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
// Checks if we should reload the collection view to fix a bug @ http://openradar.appspot.com/12954582
if (self.shouldReloadCollectionView) {
[self.collectionView reloadData];
} else {
[self.collectionView performBatchUpdates:^{
[self.blockOperation start];
} completion:nil];
}
}
@mangoldm
Copy link

Thanks very much for this, but could you explain the scope of blockOperation? It is created in controllerWillChangeContent: but used in other methods without reference to self, and I'm confused. Thanks.

@mickspecial
Copy link

make a loc. var for it

@iwasrobbed
Copy link
Author

@mangoldm As mickspecial said, it's just an instance variable in the class. Nowadays, I probably would create it as a private property in the class so it would be self.blockOperation.. More info here: https://github.com/iwasrobbed/Objective-C-CheatSheet#properties-and-variables

@DanMorganiOS
Copy link

Got to your gist from here https://github.com/AshFurrow/UICollectionView-NSFetchedResultsController/issues/13

Thank you for this! Works really well for my use case so far.

@taiyangc
Copy link

taiyangc commented Sep 9, 2014

Thanks for the integration, great gist! Just one thing, indexPath on line 44 is always nil for insert, so it should be newIndexPath. Also, I don't think line 43 is necessary as the if check already covers this case.

@lukasredev
Copy link

@AppsTitude
Copy link

UICollectionView and NSFectchedResultsControllerDelegate integration in Swift:
https://gist.github.com/AppsTitude/ce072627c61ea3999b8d

@MatejBalantic
Copy link

Hey guys, just mind that this is unsafe as it performs updates on the UICollectionViews on the non-main thread. Solution would be to schedule the execution of the blocks on the current thread:

[[NSOperationQueue currentQueue] addOperation:self.blockOperation];

@pomozoff
Copy link

@MatejBalantic, according Apple Docs:

Operation objects are synchronous by default. In a synchronous operation, the operation object does not create a separate thread on which to run its task. When you call the start method of a synchronous operation directly from your code, the operation executes immediately in the current thread. By the time the start method of such an object returns control to the caller, the task itself is complete.

@SamClewlow
Copy link

@pomozoff This is incorrect. When creating your own NSOperation subclass, it will operate synchronously if you call start, but an NSBlockOperation is different. From the NSBlockOperation Apple docs:

"The NSBlockOperation class is a concrete subclass of NSOperation that manages the concurrent execution of one or more blocks. You can use this object to execute several blocks at once without having to create separate operation objects for each......"

"Blocks added to a block operation are dispatched with default priority to an appropriate work queue. The blocks themselves should not make any assumptions about the configuration of their execution environment."

I understand that internally the block operation is dispatching the blocks to background threads, and indeed logging out the thread number from within the queued blocks yields:

2015-10-26 09:31:10.414 [76548:1833049] <NSThread: 0x7e922e80>{number = 37, name = (null)} 
2015-10-26 09:31:10.414 [76548:1833062] <NSThread: 0x7bf81b80>{number = 33, name = (null)}
2015-10-26 09:31:10.414 [76548:1831231] <NSThread: 0x7d1380b0>{number = 1, name = main}
2015-10-26 09:31:10.414 [76548:1833061] <NSThread: 0x7be2efd0>{number = 35, name = (null)}
2015-10-26 09:31:10.414 [76548:1833063] <NSThread: 0x7e823f10>{number = 36, name = (null)}
2015-10-26 09:31:10.414 [76548:1833037] <NSThread: 0x7d2c0b30>{number = 34, name = (null)}
2015-10-26 09:31:10.414 [76548:1833064] <NSThread: 0x7d5273b0>{number = 32, name = (null)}
2015-10-26 09:31:10.414 [76548:1833050] <NSThread: 0x7d720e70>{number = 38, name = (null)}

@ElGrowZone
Copy link

ElGrowZone commented Jul 27, 2016

OK. scrap that ...

@spnkr
Copy link

spnkr commented Sep 29, 2016

@MatejBalantic you're correct about using [[NSOperationQueue currentQueue] addOperation:self.blockOperation]; v.s. [self.blockOperation start];. If I use blockOperation.start, NSFetchedResultsChangeUpdate events aren't always processed properly (or at all).

Here's an updated controllerDidChangeContent method that works for me:

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
    // Checks if we should reload the collection view to fix a bug @ http://openradar.appspot.com/12954582
    if (self.shouldReloadCollectionView) {
        [self.collectionView reloadData];
    } else {
        [self.collectionView performBatchUpdates:^{
          [[NSOperationQueue currentQueue] addOperation:self.blockOperation];
        } completion:nil];
    }
}

@yigegongjiang
Copy link

Thanks.

@ygit
Copy link

ygit commented Jul 30, 2017

Hi guys, can anyone tell whats the status of the bug http://openradar.appspot.com/12954582 ?
Do we still have to use reloadData on insertion of first element to collection views in iOS 9+ ?

@fdstevex
Copy link

fdstevex commented Jun 5, 2020

I was getting errors from the thread sanitizer that blocks were running off the main thread. Changed to use an array of raw blocks and calling them directly rather than running them through an NSBlockOperation.

https://gist.github.com/fdstevex/7a782bb864b7b23b8d8a8e2393286fac

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment