Cell reuse bug in collectionView - ios

Hello to all dear friends. I'm experiencing a problem with cell reuse. In practice I select multiple cells. In the "didSelected" method, I modify a property, called "isSelectedCell", to true and add a green border (to indicate the selection); While in the "didDeselect" method I carry it to false and remove the color of the border. But when I scroll down some cells, never selected, appear to be selected and the property is true. Because? How do I prevent this. It seems that when a cell is reused, the properties take on old ones and not their own.

If you are using a custom cell, override prepareForReuse and reset all properties to default values
override func prepareForReuse() {
super.prepareForReuse()
// reset custom properties to default values
}

Cells are for reuse means they are only fixed number of views which are used again. You have to update cells for every item to be displayed in that cell in collectionView cellForItemAtIndexPath method. You've to put it inside data source like array of objects to return correct value for every item. Hence it will remember to display which property on item.
Reusable cells are necesary to not bloat up the device memory and reuse same views.

it's due to the cell reuse of CollectionView.
as you can see in this article
when a cell disappear, it's reused to be the new one that going to appear.

Related

Reusing cells with dynamic layout content at runtime

I have a type of UITableViewCell that lets the user add/remove as many UITextViews as they want at run time.
I'm running into issues when trying to reuse/dequeue cells of that type, as sometimes the tableview cells just start overlapping when you scroll up and down. When I dequeue/return the cell, I'm running a setup method (which initiates a teardown method internally first to remove all the previous views), and uses the model to setup/restore all the necessary views and layout constraints.
if let cell = tableView.dequeueReusableCell(withIdentifier: "MultipleContentCell", for: indexPath) as? MultipleChoiceTableViewCell {
cell.setupCellWithModel(model: model)
cell.setNeedsUpdateConstraints()
cell.updateConstraintsIfNeeded()
cell.delegate = self
return cell
}
I can't really figure out why that cells sometimes overlap in the tableview, but I'm guessing it has to do with the layout being recreated on the fly. I'm considering not reusing these types of cells and just storing them in a list.
My question is: are reusable cells always suppose to have the same general UIView layout, and only the content changes? Am I not supposed to use reuse these types of cells? Or has someone experienced this before?
Thanks
The UITextView are created each time you dequeue cell and never delete. To repair that use function prepareForReuse(). You have to define, what your cell should do before dequeue in MultipleChoiceTableViewCell. For example:
override func prepareForReuse() {
super.prepareForReuse()
for view in speciesName.subviews {
if view is UITableView {
view.removeFromSuperview()
}
}
}
I added similar question few days ago:
Cells in UITableView overlapping. Many cells in one place
If you have some question, I can try to help you more tomorrow.
Cheers!
In general, yes. You want the physical layout of your cells to be static, and only vary the contents when you recycle them. If you add views to your cells in cellForRow(at:) then the burden is on you to manage the extra fields to avoid duplicate views.
Your case where you add a variable number of views to a table view cell based on user interaction is an odd case where you might need to add and remove cells on the fly.
One way to handle this would be to put all of your text fields in a container view, add an outlet to that container view, and then simply use code like this in your prepareForReuse or cellForRowAt function:
containerView.subviews.forEach { $0.removeFromSuperview() }

iOS8 change height of dynamic cell "now", re internal cell content

Here's a dynamic cell
Note - in the example, the text is not data driven. It's just some text local to the cell (consider, say, a help text). At runtime, change the .text of the UILabel from one word to many lines, using a button actually inside the cell. iOS perectly resizes the cell and table....
... but only when the cell is scrolled offscreen, and then on again.
How to alert the table view to recalculate everything "now" ?
(Please note, this question ONLY in the case of iOS8+, Xcode7+, autolayout for dynamic cell heights.)
Changing height
So basically, there are two ways to do:
The first one is to actually reload the cell (not the tableview). Reloading will call new heightForRow (don't forget to purge cache, if you are caching the sizes), which will return proper new height:
let indexPaths = [NSIndexPath(forRow: ~the rows in question~, inSection: 0)]
self.table.reloadRowsAtIndexPaths(indexPaths, withRowAnimation: .Automatic)
(Note however that this often involves reloading more than one row; notably if you select/deselect, you have to reload all rows changed.)
If however you ONLY want to change the size of the cell and the content per se, and did not really change the data content ... so for example:
you clicked some button and you assigned new local text in the cell to outlets (perhaps a help text):
you changed only the LAYOUT of the cell. for example, you made a font larger, or changed the margin of a block of text so that the height of a block of text changed, so indeed the height of the overall cell changed:
In that case instead of reloading, just call the following, which forces the tableview to basically do all animations, and for that it needs new heights, so it requests it:
self.table.beginUpdates()
self.table.endUpdates()
The true solution
I see what your problem is. You are trying to change the height of the cell from the actual cell - but you will not succeed in that -> and you should not. See, the cell is view, and view should not have any idea about its data whatsoever - view is what presents. If you need any changes, you should inform your controller to do so. To do that, you can use notifications, but preferably protocols / delegates.
So at first you create protocol in your cell, which will be used to inform the controller, that there is a change:
protocol MyCellDelegate {
func buttonTappedForCell(cell : UITableViewCell)
}
Now, you need to conform to that protocol in your view controller that contains table:
class MyClassWithTableView : MyCellDelegate
Lastly, you need to declare delegate in the cell:
class MyCell {
var delegate : MyCellDelegate
}
And assign it in the configuration of the cell, which you probably have in the view controller:
cell.delegate = self
This is the basic setup for all the delegates / protocols really, and now, when you click on your button, you can forward the action to your controller:
#IBAction myButtonTouchUpInside() {
self.delegate.buttonTappedForCell(self)
}
After doing all that, proceed as in part 1. That is to say, either reloadRowsAtIndexPaths or a beginUpdates / endUpdates pair as explained above.
Hope it helps!
I'm presuming you're not setting the text property of the UILabel inside cellForRowAtIndexPath but rather somewhere else (or doing it asynchronously). If that's the case, I wouldn't update the UI there. Rather, I'd update the model backing the table and then call reloadRowsAtIndexPaths. That will let cellForRowAtIndexPath call again, but unlike reloading the whole table, this will gracefully keep the contentOffset of the tableview right where it is.
I know this all sounds unnecessarily complicated, but the bottom line is that you don't own this view, the table view does. It has to do all sorts of stuff above and beyond updating the cell. I.e., if the cell grew, figure out which cells scrolled out of view and dequeue them. If the cell shrunk, figure out which cells scrolled into view as a result.
It's a surprisingly complex dance. You can try calling setNeedsLayout on the cell, but I wouldn't expect that to work (and even if it does, it is a fragile approach). The table view is responsible for managing its cells, so if you really should just update model and reload that one cell.
did you try calling reloadRowsAtIndexPaths on the cell index? it's supposed to animate to the new size, if the constraints are setup correctly.
You should call self.tableView.reloadData() just AFTER you made the cell's label's text change.
It will force the tableView to redraw the cell's. That's what happened when you scroll, the cell is being reused, and redrawn when it comes back again.
EDIT:
If you can't or won't do a reloadData on your tableView, you can use:
self.tableView.beginUpdates()
self.tableView.reloadRowsAtIndexPaths([NSIndexPath(row:0 section:0)] withRowAnimation:UITableViewRowAnimation.Automatic)
self.tableView.endUpdates()
I dont know your code but did you really execute your ui changes on the main thread. Same problem happened to me and was solved with putting the exectuion on the main thread.
dispatch_async(dispatch_get_main_queue()) { () -> Void in
[...]
}

Multiple cells got affected after calling cellForRowAtIndexPath

I have a UITableView, and I want to change text color of the selected row. But I see every other 15 row got affected along with the one I clicked. This is tested under the master-detail sample project.
if let indexPath = self.tableView.indexPathForSelectedRow(){
self.tableView.cellForRowAtIndexPath(indexPath)?.textLabel?.textColor = UIColor.redColor()
}
I checked cellForRowAtIndexPath value in debug session, it seems returning only one object, how come other cells got affected too?
Cells are reused - almost certainly you are not resetting your cells to a base state in prepareForReuse and configuring them correctly on each call to cellForRowAtIndexPath.
When you look at a table view that can display some number of cells at once, typically there will only exist one more cell than can be shown. When a cell is moved off the screen it is placed in a pool of cells for reuse. Just before a cell moves onto the screen it is configured by you, in cellForRowAtIndexPath. If you have configured something in the cell and you do not configure that explicitly every time you return a cell from cellForRowAtIndexPath then that configuration persists in the cell that is in the reuse pool. The function prepareForReuse is also called before each cell is reused - if you have subclassed UITableViewCell then you can implement this function to return the cell to a base configuration, so that settings like text color do not unexpectedly affect multiple cells.
This approach makes it possible to scroll through an entire table view smoothly with a minimum amount of memory used. It is an expensive operation to create and destroy cells every time one disappears and a new one is required.
The simplest fix is to always set the text color in cellForRowAtIndexPath - either to the base color or to the special color you want in some cells.

UICollectionView selection does not last

I have a UICollectionView I use like a tool selection container. You have many tools (the cells), when you select one, the former selected is deselected, and so on... You can only have one tool selected at a time.
The problem is that I can't manage to have the cells selected. When I tap the cell, the collectionView:didSelectItemAtIndexPath: is called. I then reload the selected cell to change it's appearance (I change the alpha of the image) by using the [collectionView reloadItemsAtIndexPaths:#[indexPath]].
Well that reloads the cell ok, except one damn thing: in the collectionView:cellForItemAtIndexPath: the cell never get the selected property set to YES! So I can never change the alphabecause I never know when I must draw a selected cell or not.
Worse: [collectionView indexPathsForSelectedItems] is always empty!!!
I don't even mention the collectionView:didDeselectItemAtIndexPath: that is never called...
Well, if someone can help me understand what is going on, thanks in advance...
When you call reloadItemsAtIndexPaths: the collection view discards those cells and creates new ones, therefor discarding the selected state of the cells.
I'd suggest a couple of different options:
1.) In collectionView:didSelectItemAtIndexPath: call cellForItemAtIndexPath: to get a reference to the selected cell and update it's appearance there.
2.) (My Favorite) Use a custom subclass of UICollectionViewCell if you're not already, and override the method setSelected:. There you'll be notified when the cell is selected and you can update the appearance from within the subclass.
With regards to selecting multiple cell at once, you should try the allowsMultipleSelection property [myCollectionView setAllowsMultipleSelection:YES] The docs for that are here: https://developer.apple.com/library/ios/documentation/uikit/reference/UICollectionView_class/Reference/Reference.html#//apple_ref/occ/instp/UICollectionView/allowsMultipleSelection
Otherwise, #daltonclaybrook's answer is sufficient.

UITableView reusable cell or not

I'm developing an app that represents a huge amount of data using UITableView with a custom UITableViewCell.
When the tableView is set to editing mode, it supports multiple selection for further usage of the selected set of data.
Using -tableView:didSelectRowAtIndexPath: and didDeselect I'm changing the rows UIImageViews image to a little tick and storing the selection into an array. When selecting a cell it gets light blue background (iOS standard).
When I'm now scrolling down and up again, the cells background is still light blue but the image is reset to default and the cells isSelected property is NO. Selecting it again invokes the -tableView:didSelectRowAtIndexPath: method.
After a while debugging it turned out that the reusable identifier was wrong. But as I corrected it, scrolling down repeated the same 12 cells over and over again. And the isSelected property is still reset to NO.
How can I maintain selected rows while scrolling the tableView?! And: Why is the cell blue highlighted (or marked as selected) but the isSelected property gets reset to No?
Thanks for help, with kind regards, Julian
Don't use the cell to hold persistent controller state. Instead keep an NSArray and add the NSIndexPath of each selected cell to your array (and remove them if/when the user deselects a cell).
Then in cellForRowAtIndexPath: just make sure you configure the cell correctly based upon whether or not its index-path is in your array of selected cells.

Resources