33

I have a UICollectionView which I am trying to insert items into it dynamically/with animation. So I have some function that downloads images asynchronously and would like to insert the items in batches.

Once I have my data, I would like to do the following:

[self.collectionView performBatchUpdates:^{
    for (UIImage *image in images) {
        [self.collectionView insertItemsAtIndexPaths:****]
    }
} completion:nil];

Now in place of the ***, I should be passing an array of NSIndexPaths, which should point to the location of the new items to be inserted. I am very confused since after providing the location, how do I provide the actual image that should be displayed at that position?

Thank you


UPDATE:

resultsSize contains the size of the data source array, self.results, before new data is added from the data at newImages.

[self.collectionView performBatchUpdates:^{

    int resultsSize = [self.results count];
    [self.results addObjectsFromArray:newImages];
    NSMutableArray *arrayWithIndexPaths = [NSMutableArray array];

    for (int i = resultsSize; i < resultsSize + newImages.count; i++)
          [arrayWithIndexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]];

          [self.collectionView insertItemsAtIndexPaths:arrayWithIndexPaths];

} completion:nil];
zanona
  • 12,345
  • 25
  • 86
  • 141
darksky
  • 20,411
  • 61
  • 165
  • 254

3 Answers3

34

See Inserting, Deleting, and Moving Sections and Items from the "Collection View Programming Guide for iOS":

To insert, delete, or move a single section or item, you must follow these steps:

  1. Update the data in your data source object.
  2. Call the appropriate method of the collection view to insert or delete the section or item.

It is critical that you update your data source before notifying the collection view of any changes. The collection view methods assume that your data source contains the currently correct data. If it does not, the collection view might receive the wrong set of items from your data source or ask for items that are not there and crash your app.

So in your case, you must add an image to the collection view data source first and then call insertItemsAtIndexPaths. The collection view will then ask the data source delegate function to provide the view for the inserted item.

Max MacLeod
  • 26,115
  • 13
  • 104
  • 132
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • So for insertion I would need two arrays: the dataSource's array and then an array that contains the new items. I should add the new items to the data source array, and then call update with the indexpath of the first new item in the data source array. Is that correct? – darksky Sep 29 '12 at 21:58
  • @Darksky: Almost. You should add the new items to the data source array first and then call `insertItemsAtIndexPaths:` for all new items in the data source array. You can also add a single new item to the data source array, call `insertItemsAtIndexPaths:` for that item, and repeat this procedure for all items to add. Since you are doing a batch update, there will be no difference. The only important thing is: When you call `insertItemsAtIndexPaths:` then the data source must already been updated accordingly. – Martin R Sep 29 '12 at 22:15
  • 1
    I am having an issue with what you described, posted at: http://stackoverflow.com/questions/12665669/insertitemsatindexpaths-update-error – darksky Sep 30 '12 at 22:24
  • @Darksky: As I see, the issue has been solved already. Please let me know if there is anything wrong with my answer. – Martin R Oct 01 '12 at 05:13
  • 1
    I am having an issue actually. Initially, when performUpdates inserts images to an empty collection view, it displays fine. After that, subsequent updates are not inserting new data, but inserting images from positions 1-5, and then 2-6, 3-7 (I am inserting images 4 at a time) etc... instead of inserting 1-4, 5-8. So it is only moving over one position and reinserting everything there instead of skipping the old images and only inserting the new ones. My array that contains the index paths is correct. Please see the edit above which contains my code. – darksky Oct 01 '12 at 13:55
  • @Darksky: I cannot reproduce the problem. I have build a small test project with a collection view and exactly your updated code to insert items. And all items are appended to the collection view, as you would expect from the code. – Martin R Oct 01 '12 at 17:57
  • How are you inserting/appending data? As in, what is it in your data source array? – darksky Oct 01 '12 at 18:36
  • @Darksky: I have just used your updated `[self.collectionView performBatchUpdates:...]` code with `self.results` as data source. – Martin R Oct 01 '12 at 18:41
  • 8
    `UICollectionView`s are just absolutely ridiculous. All I have is a collection view with a data source array with data that I download online using GCD and update the UI in the main thread. That's it! The collection view can't even change its content size to allow scrolling to the items automatically. Can you post your whole class somewhere so I can perhaps take a look at it? I've been battling with collection views for 3 days while Apple claims they're the new table views. I am literally pulling my hair my deadline is very close. – darksky Oct 01 '12 at 18:56
  • @Darksky: See http://ul.to/4aj1yfml for some code (with text labels instead of images to make things simpler for me). – Martin R Oct 01 '12 at 19:23
  • @darksky still buggy 6 years later. couldn't scroll to the right after appending one cell. fix was to put collectionView.isScrollEnabled = collectionView.isScrollEnabled in performBatchUpdates completion block. – codrut Jul 23 '19 at 17:33
12

I just implemented that with Swift. So I would like to share my implementation. First initialise an array of NSBlockOperations:

    var blockOperations: [NSBlockOperation] = []

In controller will change, re-init the array:

func controllerWillChangeContent(controller: NSFetchedResultsController) {
    blockOperations.removeAll(keepCapacity: false)
}

In the did change object method:

    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!])
                }
            })
        )
    }
}

In the did change section method:

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

And finally, in the did controller did change content method:

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

I personally added some code in the deinit method as well, in order to cancel the operations when the ViewController is about to get deallocated:

deinit {
    // Cancel all block operations when VC deallocates
    for operation: NSBlockOperation in blockOperations {
        operation.cancel()
    }

    blockOperations.removeAll(keepCapacity: false)
}
Plot
  • 898
  • 2
  • 15
  • 26
  • I put this boilerplate code inside a dedicated class CollectionViewLayoutUpdater for reuse purposes. Thank you for sharing! – nikolsky Apr 21 '16 at 02:09
5

I was facing the similar issue while deleting the item from index and this is what i think we need to do while using performBatchUpdates: method.

1# first call deleteItemAtIndexPath to delete the item from collection view.

2# Delete the element from array.

3# Update collection view by reloading data.

[self.collectionView performBatchUpdates:^{
            NSIndexPath *indexPath = [NSIndexPath indexPathForRow:sender.tag inSection:0];
            [self.collectionView deleteItemsAtIndexPaths:[NSArray arrayWithObject:indexPath]];
            [self.addNewDocumentArray removeObjectAtIndex:sender.tag];
        } completion:^(BOOL finished) {
            [self.collectionView reloadData];
        }];

This help me to remove all the crash and assertion failures.

Nik
  • 1,679
  • 1
  • 20
  • 36
  • 5
    Reloading the collectionview is not needed after the insertion or deletion. But your solution is working very well. – Myrddin Nov 08 '16 at 11:03
  • 1
    This is not the way to do it. All you need to do is (1) Delete the element from the array, and then (2) call -deleteItemsAtIndexPaths:. You don't need to wrap it in -performBatchUpdates, and you don't need to call -reloadData either. – Lukas Petr Mar 25 '17 at 04:05
  • @LukasPetr I think the question is "How to use performBatchUpdates in collectionView". For above question the way I handled, it helps me to remove crashes as well as assertion failures. – Nik Mar 31 '17 at 04:25
  • @Nik, this is the only way to do it, it looks like other's don't understand what the issue is, this is the only way to update the indexes correctly when you're working with blocks inside of custom collection cells, very nice – Larry Pickles Jun 22 '17 at 15:16
  • 3
    This code will remove crashes, but you are reloading the entire table afterwards. You may as well just call [self.collectionView reloadData];.... which does not give you the removal effect. – Francois Nadeau Jul 21 '17 at 16:09
  • 1
    this is 100% wrong: inserting/deleting an object is exactly to avoid the reload. – Matteo Gobbi Jul 09 '23 at 22:29