My application resembles Apple Books: when a user removes a book from the top shelf, the book fills up the free space from the bottom shelf. And all this should be with animation. The appearance of my application looks like this (below are the screenshots of the interface with explanations): there is a UICollectionView with two sections (section 0, section 1). In each section there are 5 items. When you try to remove an item from the second section (section 1), everything works perfectly. But when I delete an item from the upper section (section 0), I get the following error:

There is a number of items in the section after the update (5), plus or minus (0 inserted, 1 deleted) (0 moved in, 0 moved out). with userInfo (null)

I do everything according to the instructions: 1. delete my item from the database (Core Data), 2. update the array with items, 3. delete the item from the collectionView. Here is the code with these actions:

extension MainCollectionViewController: NSFetchedResultsControllerDelegate { func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) { collectionView?.performBatchUpdates({ [weak self] in guard let self = self else { return } items = controller.fetchedObjects as! [Item] items2 = items.chunked(into: 5) self.collectionView?.deleteItems(at: [self.deletedItemIndex!]) }) } } extension Array { func chunked(into size: Int) -> [[Element]] { return stride(from: 0, to: count, by: size).map { Array(self[$0 ..< Swift.min($0 + size, count)]) } } } 

The chunked function works as follows: it creates subarrays in an array, where each subarray is associated with a section. It looks like this:

Until chunked:

 [1,2,3,4,5,6,7,8,9,10] //исходный массив items 

After chunked (array items2):

 [ [1, 2, 3, 4, 5], // первая секция (section 0) в collectionView [6, 7, 8, 9, 10], // вторая секция (section 1) в collectionView ] 

So, for example, if you remove the number 3 from the source array (items) and apply the chunked function to this array, then the items2 array will look like this:

 [ [1, 2, 4, 5, 6], // первая секция (section 0) в collectionView [7, 8, 9, 10], // вторая секция (section 1) в collectionView ] 

Thus, the first subarray is always initialized with 5 elements. Filling the collection looks like this (it's simple):

  override func numberOfSections(in collectionView: UICollectionView) -> Int { print("call numberOfSections") //3 return items2.count } override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { print("call numberOfItemsInSection, current section is \(section)") //4 return items2[section].count } override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! CollectionViewCell let item = items2[indexPath.section][indexPath.row] cell.itemNameTextLabel.text = item.name cell.itemImageView.image = UIImage(data: item.image! as Data) return cell } } 

Deleting an item occurs after a long press on it (here is also a trivial code):

 @objc func handleLongPress(gesture: UILongPressGestureRecognizer!) { if gesture.state != .ended { return } let p = gesture.location(in: self.collectionView) if let indexPath = self.collectionView?.indexPathForItem(at: p) { let fetchRequest: NSFetchRequest<Item> = Item.fetchRequest() fetchRequest.predicate = NSPredicate(format: "name == %@", self.items2[indexPath.section][indexPath.row].name!) do { if let context = (UIApplication.shared.delegate as? AppDelegate)?.persistentContainer.viewContext { let selectedItem = try context.fetch(fetchRequest)[0] //save deleted item index in var that use it index in performBatchUpdatesBlock deletedItemIndex = IndexPath(row: indexPath.row, section: indexPath.section) context.delete(selectedItem) do { try context.save() print("Save!") } catch let error as NSError { print("Oh, error! \(error), \(error.userInfo)") } } } catch { print(error.localizedDescription) } } } 

The array items2 is updated correctly, if you delete an item from the first section (section 0) (it will be 4), you can see it if you write this line

 print(self.items2[0].indices) 

in the performBatchUpdates method after the line

 items2 = items.chunked(into: 5) 

(that is, after updating the items2 array). The result will be

 0..<5 

that is, the first subarray took an element from the second subarray and appropriated it to itself, all as it should! But for some reason the application crashes with the above error ... Here is a picture with the interface: enter image description here

If in the performBatchUpdates method to register such a code (after deleting the old code inside this method):

 items2[deletedItemIndex!.section].remove(at: deletedItemIndex!.row) self.collectionView?.deleteItems(at: [self.deletedItemIndex!]) 

then the result will be as such (the result is shown here already after the user has removed the potatoes from the first section): enter image description here

The items2 array was updated (but did not fill its empty cells in the first section, since we didn’t ask for it) and the numberOfItemsInSection method worked correctly (since we didn’t ask it to return 5 elements), but when trying to sort this array (using chunked function), so that there are 5 elements in its first section - this error occurs.

Here is a link to GitHub with a project (if you run it, then on the iPhone SE simulator, please). The data is registered in items.plist and automatically saved to your local database when you first start the application.

Question: what is wrong with my code?

  • 2
    A sample of the elegantly posed question. - VAndrJ
  • @VAndrJ and this you with sarcasm?) - Tkas
  • one
    No, I'm serious. Everything is painted, examples of the code in question, screenshots of the result, error, project with a reproducing error. - VAndrJ

1 answer 1

You have a problem with the fact that you delete an element:

 self.collectionView?.deleteItems(at: [self.deletedItemIndex!]) 

For the rest of the description, the quantity should remain unchanged in this section. Thus, the UICollectionView expects that there should already be 4 elements, but there are still 5 of them. Solution options:

  1. Correctly replace. Those. when deleting the last cell in the first section, you must "move" the first cell of the second section to its place, and remove all the cells of the second section to the beginning of the section and the last cell of the second section. Method

    moveItem (at: to :)

  2. To use, for example, Differ , which will calculate everything for you.
    Made a pull request with an example. Link to fork:

Github

Result:

enter image description here

  • Thank you for your advice! the moveItem method (at: to :) it turns out that I will have to create a loop in the performer Batche where I will shift the cells by this method? I just never used the MoviAitem method - Tkas
  • got you And Differ is ... is that?) If you, of course, have time to explain - Tkas
  • one
    @Tkas This is one of the libraries that makes life easier. In the example, find the collectionView?.animateItemAndSectionChanges(oldData: oldData, newData: items2) . Those. transferred the old data, transferred the new, it all cheated and deleted / added / moved. - VAndrJ
  • that is, this line of collectionView.animateItemAndSectionChanges (oldData: old, newData: new) is equivalent to a loop with movie? - Tkas
  • one
    @Tkas added edits with comments to animate the shift. Rebooting with sections is caused by the fact that when comparing normal arrays, it sees that the arrays are different and reloads the entire section. With an additional wrapper, we specify how to deal with sections, and with elements already. - VAndrJ