How to delete the only cell from UICollectionView with animation (deleteItemsAtIndexPaths)? - ios

I'm getting an assertion while deleting a single UICollecitonViewCell from UICollectionView.
Precondition: I have a single cell (when I have two or more cells, the deletion works good).
Here is the code:
NSIndexPath *ip = [_photosCollectionView indexPathForCell:cell];
[_datasource removeItemAtIndex:ip.item];
[_photosCollectionView deleteItemsAtIndexPaths:#[ip]]; // the assertion is raised
Here is the assertion text:
NSInternalInconsistencyException: attempt to delete item 0 from section 0 which only contains 0 items before the update
Quite strange issue, because it works for 2, 3 or more cells, but when I delete a single cell, it fails.
Any ideas what's wrong, how to work-around this issue?

Thanks to similar questions and answers on SO, found a solution to use performBatchUpdates:
[_photosCollectionView performBatchUpdates:^ {
[_datasource removeItemAtIndex:ip.item];
[_photosCollectionView deleteItemsAtIndexPaths:#[ip]]; // no assertion now
} completion:nil];

Same solution with Swift 4.1
let indexPath = IndexPath(item: 0, section: 0)
self.collectionView.performBatchUpdates({
self.collectionView.deleteItems(at:[indexPath])
}, completion:nil)

Related

scrollToRowAtIndexPath throwing error

UITableView _contentOffsetForScrollingToRowAtIndexPath:atScrollPosition:]: section (3) beyond bounds (1).'
I've got an array of information being displayed at my tableview, and when I hit a button I'm wanting it to scroll down to a certain cell.
Initially I thought I was getting that error because of the way I was getting my index to jump to:
if let index = peopleListArray.index(where: { $0.UID == currentUser }) {
print(index)
let NSIndex = NSIndexPath(index: Int(index) )
tableView.reloadData()
print(peopleListArray.count)
print(NSIndex)
tableView.scrollToRow(at: NSIndex as IndexPath , at: .top, animated: true )
}
But then I replaced "let NSINDex... with
let NSIndex = NSIndexPath(index: 1 )
and it's still throwing the same error.
when I'm printing out my array count and my NSIndex I'm always getting an 8 for the count (which is correct) and I'm getting 3 for the NSINdexPath which is correct.
I could understand the error if the 3 was out of bounds of my array.count but it definitely isn't.
any ideas?
It seems the issue you are having is with the section and not with the row. Try to build the index like this:
NSIndexPath(item: index, section: 0)
Note the section is set to 0.
There are two options that may be wrong here:
you passed a section that is out of bounds, i.e with index less than 0 or bigger than the specified number of sections in
numberOfSectionsInTableView:(UITableView *)tableView
So just try to use 0 for the section number first.
the index of the row is out of bounds for the specified section (they can be different for each section)

Nightmare with performBatchUpdates crash

I am facing a nightmare of a crash during performBatchUpdates on a collection view.
The problem is basically this: I have a lot of images on a directory on a server. I want to show the thumbnails of those files on a collection view. But the thumbnail have to be downloaded from the server asynchronously. As they arrive they will be inserted on the collection view using something like this:
dispatch_async(dispatch_get_main_queue(),
^{
[self.collectionView performBatchUpdates:^{
if (removedIndexes && [removedIndexes count] > 0) {
[self.collectionView deleteItemsAtIndexPaths:removedIndexes];
}
if (changedIndexes && [changedIndexes count] > 0) {
[self.collectionView reloadItemsAtIndexPaths:changedIndexes];
}
if (insertedIndexes && [insertedIndexes count] > 0) {
[self.collectionView insertItemsAtIndexPaths:insertedIndexes];
}
} completion:nil];
});
the problem is this (I think). Suppose that at time = 0, the collection view has 10 items. I then add 100 more files to the server. The application sees the new files and start downloading the thumbnails. As the thumbnails download they will be inserted on the collection view. But because the downloads can take different times and this download operation is asynchronous, at one point iOS will lost track of how many elements the collection has and the whole thing will crash with this catastrophic infamous message.
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid
number of items in section 0. The number of items contained in an
existing section after the update (213) must be equal to the number of
items contained in that section before the update (154), plus or minus
the number of items inserted or deleted from that section (40
inserted, 0 deleted) and plus or minus the number of items moved into
or out of that section (0 moved in, 0 moved out).'
The proof I have something fishy is going on is that if I print the count of items on the data set I see exactly 213. So, the dataset matches the correct number and the message is nonsense.
I have had this problem before, here but that was an iOS 7 project. Somehow the problem returned now on iOS 8 and the solutions there are not working and now the dataset IS IN SYNC.
It sounds like you need to do a bit extra work with batching which images have appeared for each animation group. From dealing with crashes like this before, the way performBatchUpdates works is
Before invoking your block, it double checks all the item counts and saves them by calling numberOfItemsInSection (this is the 154 in your error message).
It runs the block, tracking the inserts/deletes, and calculates what the final number of items should be based on the insertions and deletions.
After the block is run, it double checks the counts it calculated to the actual counts when it asks your dataSource numberOfItemsInSection (this is the 213 number). If it doesn't match, it will crash.
Based on your variables insertedIndexes and changedIndexes, you're pre-calculating which things need to show up based on the download response from server, and then running the batch. However I'm guessing your numberOfItemsInSection method is always just returning the 'true' count of items.
So if a download completes during step 2, when it performs the sanity check in '3', your numbers won't line up anymore.
Easiest solution: Wait until all files have downloaded, then do a single batchUpdates. Probably not the best user experience but it avoids this issue.
Harder solution: Perform batches as needed, and track which items have already shown up / are currently animating separately from the total number of items. Something like:
BOOL _performingAnimation;
NSInteger _finalItemCount;
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return _finalItemCount;
}
- (void)somethingDidFinishDownloading {
if (_performingAnimation) {
return;
}
// Calculate changes.
dispatch_async(dispatch_get_main_queue(),
^{
_performingAnimation = YES;
[self.collectionView performBatchUpdates:^{
if (removedIndexes && [removedIndexes count] > 0) {
[self.collectionView deleteItemsAtIndexPaths:removedIndexes];
}
if (changedIndexes && [changedIndexes count] > 0) {
[self.collectionView reloadItemsAtIndexPaths:changedIndexes];
}
if (insertedIndexes && [insertedIndexes count] > 0) {
[self.collectionView insertItemsAtIndexPaths:insertedIndexes];
}
_finalItemCount += (insertedIndexes.count - removedIndexes.count);
} completion:^{
_performingAnimation = NO;
}];
});
}
The only thing to solve after that would be to make sure you run one final check for leftover items if the last item to download finished during an animation (maybe have a method performFinalAnimationIfNeeded that you run in the completion block)
I think the problem is caused by the indexes.
Key:
For updated and deleted items, the indexes have to be the indexes of original items.
For inserted items, the indexes have to be the indexes of final items.
Here is a demo code with comments:
class CollectionViewController: UICollectionViewController {
var items: [String]!
let before = ["To Be Deleted 1", "To Be Updated 1", "To Be Updated 2", "To Be Deleted 2", "Stay"]
let after = ["Updated 1", "Updated 2", "Added 1", "Stay", "Added 2"]
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Refresh", style: .Plain, target: self, action: #selector(CollectionViewController.onRefresh(_:)))
items = before
}
func onRefresh(_: AnyObject) {
items = after
collectionView?.performBatchUpdates({
self.collectionView?.deleteItemsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0), NSIndexPath(forRow: 3, inSection: 0), ])
// Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to delete and reload the same index path
// self.collectionView?.reloadItemsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0), NSIndexPath(forRow: 1, inSection: 0), ])
// NOTE: Have to be the indexes of original list
self.collectionView?.reloadItemsAtIndexPaths([NSIndexPath(forRow: 1, inSection: 0), NSIndexPath(forRow: 2, inSection: 0), ])
// Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to insert item 4 into section 0, but there are only 4 items in section 0 after the update'
// self.collectionView?.insertItemsAtIndexPaths([NSIndexPath(forRow: 4, inSection: 0), NSIndexPath(forRow: 5, inSection: 0), ])
// NOTE: Have to be index of final list
self.collectionView?.insertItemsAtIndexPaths([NSIndexPath(forRow: 2, inSection: 0), NSIndexPath(forRow: 4, inSection: 0), ])
}, completion: nil)
}
override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items.count
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("MyCell", forIndexPath: indexPath)
let label = cell.viewWithTag(100) as! UILabel
label.text = items[indexPath.row]
return cell
}
}
For anyone having a similar issue, let me quote the documentation on UICollectionView:
If the collection view's layout is not up to date before you call this method, a reload may occur. To avoid problems, you should update your data model inside the updates block or ensure the layout is updated before you call performBatchUpdates(_:completion:).
I was originally referencing an array of a separate model object, but decided to keep a local copy of the array within the view controller and update the array within performBatchUpdates(_:completion:).
Problem was solved.
This may be happening because you do need to also make sure with collectionViews to delete and insert sections. when you try to insert an item in a section that doesn't exist you will get this crash.
Preform Batch updates doesn't know that you meant to add section X+1 when you insert an item at X+1, X. without you already having added that section in.

Hide view in specific CollectionViewCell

I have question how to implement this stuff in right way.
So far I done
if indexPath.row == 1 {
let indexPatha = NSIndexPath(forRow: 0, inSection: 0)
let changeCell = collectionView .cellForItemAtIndexPath(indexPatha) as! BarCollectionViewCell
changeCell.addNewBottleSecondButton.alpha = 0
}
But when I swipe until cell is hidden, I am getting error, unexpectedly found nil while unwrapping an Optional value, and still this doesn't looks like how I want to make it.
I want to achieve that when I have more then one cell, I want to hide one specific view.
Would it work in your flow to handle this in cellForRowAtIndexPath instead?
After your initialize your cell:
cell.addNewBottleSecondButton.hidden = (indexPath.row == 0) && (dataItems.count > 1)
If the cell has been scrolled off the screen it may not exist anymore due to Apple's dequeue/re-use optimizations. Previously when confronted with this problem, I've had to set a state variable and handle the UI change in cellForRow if the cell didn't exist when trying to change the cell's UI.

Iterate over all the UITableCells given a section id

Using Swift, how can I iterate over all the UITableCells given a section id (eg: all cells in section 2)?
I only see this method: tableView.cellForRowAtIndexPath, which returns 1 cell given the absolute index, so it doesn't help.
Is there an elegant and easy way?
Note: I want to set the AccesoryType to None for all of the cells in a section, programatically, say: after a button is clicked, or after something happends (what happends is not relevant for the question)
I have the reference for the UITableView and the index of the section.
You misunderstand how table views work. When you want to change the configuration of cells, you do not modify the cells directly. Instead, you change the data (model) for those cells, and then tell your table view to reload the changed cells.
This is fundamental, and if you are trying to do it another way, it won't work correctly.
You said "I need the array of cells before modifying them…" Same thing applies. You should not store state data in cells. As soon as a user makes a change to a cell you should collect the changes and save it to the model. Cells can scroll off-screen and their settings can be discarded at any time.
#LordZsolt was asking you to show your code because from the questions you're asking it's pretty clear you are going about things the wrong way.
EDIT:
If you are convinced that you need to iterate through the cells in a section then you can ask the table view for the number of rows in the target section, then you can loop from 0 to rows-1, asking the table view for each cell in turn using the UITableView cellForRowAtIndexPath method (which is different than the similarly-named data source method.) That method will give you cells that are currently visible on the screen. You can then make changes to those cells.
Note that this will only give you the cells that are currently on-screen. If there are other cells in your target section that are currently not visible those cells don't currently exist, and if the user scrolls, some of those cells might be created. For this reason you will need to save some sort of state information to your model so that when you set up cells from the target section in your datasource tableView:cellForRowAtIndexPath: method you can set them up correctly.
For Swift 4 I have been using something along the lines of the following and it seems to work pretty well.
for section in 0...self.tableView.numberOfSections - 1 {
for row in 0...self.tableView.numberOfRows(inSection: section) - 1 {
let cell = self.tableView.cellForRow(at: NSIndexPath(row: row, section: section) as IndexPath)
print("Section: \(section) Row: \(row)")
}
}
Im using same way of iterating all table view cells , but this code worked for only visible cells , so I'v just add one line allows iterating all table view cells wether visible they are or not
//get section of interest i.e: first section (0)
for (var row = 0; row < tableView.numberOfRowsInSection(0); row++)
{
var indexPath = NSIndexPath(forRow: row, inSection: 0)
println("row")
println(row)
//following line of code is for invisible cells
tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: UITableViewScrollPosition.Top, animated: false)
//get cell for current row as my custom cell i.e :roomCell
var cell :roomCell = tableView.cellForRowAtIndexPath(indexPath) as roomCell
}
* the idea is to scroll tableview to every row I'm receiving in the loop so, in every turn my current row is visible ->all table view rows are now visible :D
To answer my own question: "how can I iterate over all the UITableCells given a section id?":
To iterate over all the UITableCells of a section section one must use two methods:
tableView.numberOfRowsInSection(section)
tableView.cellForRowAtIndexPath(NSIndexPath(forRow: row, inSection: section))
So the iteration goes like this:
// Iterate over all the rows of a section
for (var row = 0; row < tableView.numberOfRowsInSection(section); row++) {
var cell:Cell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: row, inSection: section))?
// do something with the cell here.
}
At the end of my question, I also wrote a note: "Note: I want to set the AccesoryType to None for all of the cells in a section, programatically". Notice that this is a note, not the question.
I ended up doing that like this:
// Uncheck everything in section 'section'
for (var row = 0; row < tableView.numberOfRowsInSection(section); row++) {
tableView.cellForRowAtIndexPath(NSIndexPath(forRow: row, inSection: section))?.accessoryType = UITableViewCellAccessoryType.None
}
If there is a more elegant solution, go ahead and post it.
Note: My table uses static data.
Swift 4
More "swifty", than previous answers. I'm sure this can be done strictly with functional programming. If i had 5 more minutes id do it with .reduce instead. ✌️
func cells(tableView:UITableView) -> [UITableViewCell]{
var cells:[UITableViewCell] = []
(0..<tableView.numberOfSections).indices.forEach { sectionIndex in
(0..<tableView.numberOfRows(inSection: sectionIndex)).indices.forEach { rowIndex in
if let cell:UITableViewCell = tableView.cellForRow(at: IndexPath(row: rowIndex, section: sectionIndex)) {
cells.append(cell)
}
}
}
return cells
}
Im using this way of iterating all table view cells , but this code worked for only visible cells , so I'v just add one line allows iterating all table view cells wether visible they are or not
//get section of interest i.e: first section (0)
for (var row = 0; row < tableView.numberOfRowsInSection(0); row++)
{
var indexPath = NSIndexPath(forRow: row, inSection: 0)
println("row")
println(row)
//following line of code is for invisible cells
tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: UITableViewScrollPosition.Top, animated: false)
//get cell for current row as my custom cell i.e :roomCell
var cell :roomCell = tableView.cellForRowAtIndexPath(indexPath) as roomCell
}
* the idea is to scroll tableview to every row I'm receiving in the loop so, in every turn my current row is visible ->all table view rows are now visible
You can use
reloadSections(_:withRowAnimation:) method of UITableView.
This will reload all the cells in the specified sections by calling cellForRowAtIndexPath(_:). Inside that method, you can do whatever you want to those cells.
In your case, you can apply your logic for setting the appropriate accessory type:
if (self.shouldHideAccessoryViewForCellInSection(indexPath.section)) {
cell.accessoryType = UITableViewCellAccessoryTypeNone
}
I've wrote a simple extension based on Steve's answer. Returns the first cell of given type (if any) in a specified section.
extension UITableView {
func getFirstCell<T: UITableViewCell>(ofType type: T.Type, inSection section: Int = 0) -> T? {
for row in 0 ..< numberOfRows(inSection: section) {
if let cell = cellForRow(at: IndexPath(row: row, section: section)) as? T {
return cell
}
}
return nil
}
}

How to prevent moving of a UITableViewCell to a specific index

In my application, the user has the ability to move UITableViewCell rows around using the edit button and drag and drop.
I do not want the user to be able to move a cell to row 0. I already have this working for my NSMutableArray where if the row is 0 then don't rearrange the objects in collection. But even with that in place, the visible table still shows the cell at row 0.
How can I prevent this graphically?
I've tried the following:
-(NSIndexPath*)tableView:(UITableView*)tableView targetIndexPathForMoveFromRowAtIndexPath:(NSIndexPath*)sourceIndexPath toProposedIndexPath:(NSIndexPath*)proposedDestinationIndexPath
{
if(proposedDestinationIndexPath.row == 0)
{
return 1;
}
return proposedDestinationIndexPath;
}
But it crashes with a EXC_BAD_ACCESS error when I try to move cell at row 1 to row 0.
Your approach is correct. The problem is that tableView:targetIndexPathForMoveFromRowAtIndexPath:toProposedIndexPath method should return NSIndexPath*, but you return an integer. The fix is simple:
if(proposedDestinationIndexPath.row == 0)
{
return [NSIndexPath indexPathForRow:1 inSection: proposedDestinationIndexPath.section];
}
Swift Version
if(proposedDestinationIndexPath.row == 0){
return IndexPath.init(row: 1, section: proposedDestinationIndexPath.section)}

Resources