Wednesday, June 13, 2018

Issue deleting cells from UICollectionViewController

Leave a Comment

I am using UICollectionViewController and I am trying to use deleteItems in a loop of selected items like so:

for item in (collectionView?.indexPathsForSelectedItems)!         {              let cell = self.collectionView?.cellForItem(at: item) as! ImageCollectionCell              deleteLandGradingImage(images: ImagesData(jobNo: self.jobNo, ImageBytes: (self.array[item.item]["imageBytes"] as! String), Username: appDelegate.username)) { result in                  cell.imageView.layer.borderWidth = 0                 self.navigationItem.rightBarButtonItems = [self.editButton]                 self.editButton.isEnabled = true                 self.editButton.title = "Edit"                 self.doneButton.title = "Done"                  self.array.remove(at: item.item)                 self.collectionView?.deselectItem(at: item, animated: true)                 self.collectionView?.deleteItems(at: [item])              }          }   deleteLandGradingImage(images: ImagesData(jobNo: self.jobNo, ImageBytes: (self.array[item.item]["imageBytes"] as! String), Username: appDelegate.username)) { result in } 

Is a call to an API that deletes the image inside the cell and removes it from the database.

func deleteLandGradingImage(images: ImagesData, completionHandler:@escaping (_ result:Bool) -> Void) {          //Define array for returning data          var returnedResults = Bool()          //Call API          WebService().deleteLandGradingImages(images: images)         {             (result: Bool) in              DispatchQueue.main.async {                  //Return our results                  returnedResults = result                 completionHandler(returnedResults)              }          }      } 

and here is the method in WebService:

func deleteLandGradingImages(images: ImagesData, completion: @escaping (_ result: Bool) -> Void)     {          var jsonDict = [AnyHashable: Any]()          //Set Lot          jsonDict["jobNo"] = images.jobNo          //Set Image          jsonDict["imageBytes"] = images.ImageBytes          let jsonData: Data? = try? JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted)          let urlComponents = NSURLComponents(string: webservice + "DeleteGradingImages");          //Assign Username          urlComponents?.user = appDelegate.username;          //Assign Password          urlComponents?.password = appDelegate.password;          //Define our URL String          let url = urlComponents?.url;          //Define URL Request          var request = URLRequest(url: url!)          //Set Header Values for request          request.setValue("application/json", forHTTPHeaderField: "Content-Type")          //Set Header Values for request          request.setValue("application/json", forHTTPHeaderField: "Accept")          //Set Request Method to POST          request.httpMethod = "POST"          //Set Request Body to JSON Data          request.httpBody = jsonData          Alamofire.request(request)             .responseJSON { response in                  if(response.error != nil)                 {                     completion(false)                 }                 else                 {                     let responseString = (String(data: response.data!, encoding: .utf8) != nil) as Bool                     completion(responseString)                 }          }     } 

My problem with the loop is that sometimes it works, sometimes I get this error:

Invalid update: invalid number of items in section 0. The number of items contained in an existing section after the update (1) must be equal to the number of items contained in that section before the update (1), plus or minus the number of items inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out).

Sometimes I get this error:

Thread 1: Fatal error: Index out of range

on this line

self.array.remove(at: item.item) 

What am I doing wrong?

UPDATE

I got this from an answer:

@IBAction func deleteButtonPressed(_ sender: Any) {          let group = DispatchGroup()          let indexPaths = (collectionView?.indexPathsForSelectedItems)!          var deletes = Array<Dictionary<String, Any>>()          for item in indexPaths {             deletes.append(self.array[item.item])         }          for item in deletes {              group.enter()              deleteLandGradingImage(images: ImagesData(jobNo: self.jobNo, ImageBytes: (item["imageBytes"] as! String), Username: appDelegate.username)) { result in                  let i = self.array.index { (dic) -> Bool in                      return true                  }                 if i != nil {                     self.array.remove(at: i!)                     group.leave()                  }              }          }          group.notify(queue: .main) {             for item in indexPaths {                 let cell = self.collectionView?.cellForItem(at: item) as! ImageCollectionCell                 cell.imageView.layer.borderWidth = 0                 self.collectionView?.deselectItem(at: item, animated: true)             }             self.navigationItem.rightBarButtonItems = [self.editButton]             self.editButton.isEnabled = true             self.editButton.title = "Edit"             self.doneButton.title = "Done"             self.collectionView?.deleteItems(at: indexPaths)              if(self.array.count == 0)             {                 self.imageCollectionDelegate?.ImageCollectionChange(false)             }          }      } 

But sometimes I get an error on this line:

let cell = self.collectionView?.cellForItem(at: item) as! ImageCollectionCell 

This is the error:

Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value

I noticed this only happens on iPhone and if I select images and scroll through the UICollectionView, then click the delete button.

UPDATE

I have also tried this:

@IBAction func deleteButtonPressed(_ sender: Any) {          self.createIndicator()          for item in (collectionView?.indexPathsForSelectedItems)! {              if let cell = self.collectionView?.cellForItem(at: item) as! ImageCollectionCell? {                 deleteLandGradingImage(images: ImagesData(jobNo: self.jobNo, ImageBytes: (self.array[item.item]["imageBytes"] as! String), Username: appDelegate.username)) { result in                      cell.imageView.layer.borderWidth = 0                      self.collectionView?.performBatchUpdates({                         self.array.remove(at: item.item)                         self.collectionView?.deselectItem(at: item, animated: true)                         self.collectionView?.deleteItems(at: [item])                     }) { completed in                         self.navigationItem.rightBarButtonItems = [self.editButton]                         self.editButton.isEnabled = true                         self.editButton.title = "Edit"                         self.doneButton.title = "Done"                         self.stopIndicator()                      }                 }             }         }      } 

But I get this error:

Thread 1: Fatal error: Index out of range

On this line:

self.array.remove(at: item.item) 

4 Answers

Answers 1

There are two problems:

  1. Updating collectionView in loop which uses collectionView?.indexPathsForSelectedItems
  2. Calling collectionView?.deleteItems(at: [item]) too often

Best choice is to perform network requests for all indexes and then remove them from collectionView in one step. You can combine requests in one chain with DispatchGroup.

Some explanation: Removing two items. 1 and 2. Sometimes 1 will be removed first, and after that indexPathsForSelectedItems will contains just one index(1). But loop will try to remove item at indexPath 2. It's a crash.

Sample of using DispatchGroup with your code:

let group = DispatchGroup() let indexPaths = (collectionView?.indexPathsForSelectedItems)! var deletes = [<your type>]() for item in indexPaths {     deletes.append(array[item.item]) } // TODO: Block UI with UIActivityIndicatorView for item in deletes {     group.enter()     deleteLandGradingImage(images: ImagesData(jobNo: self.jobNo, ImageBytes: (item["imageBytes"] as! String), Username: appDelegate.username)) { result in         let i = self.array.index(of: item)!         self.array.remove(at: i)         group.leave()     } }  group.notify(queue: .main) {     self.navigationItem.rightBarButtonItems = [self.editButton]     self.editButton.isEnabled = true     self.editButton.title = "Edit"     self.doneButton.title = "Done"     self.collectionView?.deleteItems(at: indexPaths) } 

Answers 2

If you getting the error of

invalid number of items in section

its because, with any collection change, it expect to have the same number of items than it's source array, you can solve this by using performBatchUpdates:

for item in (collectionView?.indexPathsForSelectedItems)! {      if let cell = self.collectionView?.cellForItem(at: item) as? ImageCollectionCell {         deleteLandGradingImage(images: ImagesData(jobNo: self.jobNo, ImageBytes: (self.array[item.item]["imageBytes"] as! String), Username: appDelegate.username)) { result in              cell.imageView.layer.borderWidth = 0              collectionView?.performBatchUpdates({                 self.array.remove(at: item.item)                 self.collectionView?.deselectItem(at: item, animated: true)                 self.collectionView?.deleteItems(at: [item])             }) { completed in                 self.navigationItem.rightBarButtonItems = [self.editButton]                 self.editButton.isEnabled = true                 self.editButton.title = "Edit"                 self.doneButton.title = "Done"              }         }     } } 

Answers 3

As all answers above said - you need to use performBatchUpdates method for UICollectionView update.

From documentation:

You can use this method in cases where you want to make multiple changes to the collection view in one single animated operation, as opposed to in several separate animations. You might use this method to insert, delete, reload, or move cells or use it to change the layout parameters associated with one or more cells. Use the block passed in the updates parameter to specify all of the operations you want to perform.

That's simply mean that you need to delete all your items in one time inside performBatchUpdates closure.

You can use this code to do that:

var indexes: [IndexPath] = [] for item in (collectionView?.indexPathsForSelectedItems)! {     if let cell = self.collectionView?.cellForItem(at: item) as? ImageCollectionCell {         deleteLandGradingImage(images: ImagesData(jobNo: self.jobNo, ImageBytes: (self.array[item.item]["imageBytes"] as! String), Username: appDelegate.username)) { result in             cell.imageView.layer.borderWidth = 0             self.collectionView?.deselectItem(at: item, animated: true)             indexes.append(item.item)         }     } } indexes.forEach { self.array.remove(at: $0) } collectionView?.performBatchUpdates({     self.collectionView?.deleteItems(at: [indexes]) }) { completed in     self.navigationItem.rightBarButtonItems = [self.editButton]     self.editButton.isEnabled = true     self.editButton.title = "Edit"     self.doneButton.title = "Done" } 

Answers 4

I believe that you should first remove the item from the array than update the database I had a similar issue with my project and this simple change solved the issue. I try to post some code when I get home. Hope this helps.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment