UICollectionView cell selection and cell reuse

IosObjective CUicollectionview

Ios Problem Overview


Upon cell selection, I want to handle changing the cell appearance. I figured the delegate method collectionView:didSelectItemAtIndexPath: & collectionView:didDeselectItemAtIndexPath: is where I should edit the cell.

-(void)collectionView:(UICollectionView *)collectionView 
       didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
    
    DatasetCell *datasetCell = 
      (DatasetCell *)[collectionView cellForItemAtIndexPath:indexPath];
    
    [datasetCell replaceHeaderGradientWith:[UIColor skyBlueHeaderGradient]];
    datasetCell.backgroundColor = [UIColor skyBlueColor];
}

###and

-(void)collectionView:(UICollectionView *)collectionView 
       didDeselectItemAtIndexPath:(NSIndexPath *)indexPath {
    
    DatasetCell *datasetCell = 
      (DatasetCell *)[collectionView cellForItemAtIndexPath:indexPath];
    
    [datasetCell replaceHeaderGradientWith:[UIColor grayGradient]];
    datasetCell.backgroundColor = [UIColor myDarkGrayColor];
}

This works fine, except when the cell gets reused. If I select cell at index (0, 0), it changes the appearance but when I scroll down, there is another cell in the selected state.

I believe I should use the UICollectionViewCell method -(void)prepareForReuse to prep the cell for resuse (ie, set the cell appearance to non selected state) but its giving me difficulties.

-(void)prepareForReuse {
    if ( self.selected ) {
        [self replaceHeaderGradientWith:[UIColor skyBlueHeaderGradient]];
        self.backgroundColor = [UIColor skyBlueColor];
    } else {
        [self replaceHeaderGradientWith:[UIColor grayGradient]];
        self.backgroundColor = [UIColor myDarkGrayColor];
    }
}

When I scroll back to the top, the cell at index (0, 0) is in the deselected state.

When I just used the cell.backgroundView property, to prevent this from happening was to:

-(void)prepareForReuse {
    self.selected = FALSE;
}

and the selection state worked as intended.

Any ideas?

Ios Solutions


Solution 1 - Ios

Your observation is correct. This behavior is happening due to the reuse of cells. But you dont have to do any thing with the prepareForReuse. Instead do your check in cellForItem and set the properties accordingly. Some thing like..

 - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cvCell" forIndexPath:indexPath];


if (cell.selected) {
     cell.backgroundColor = [UIColor blueColor]; // highlight selection 
}
else
{
     cell.backgroundColor = [UIColor redColor]; // Default color
}
return cell;
}

-(void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath  {

    UICollectionViewCell *datasetCell =[collectionView cellForItemAtIndexPath:indexPath];
    datasetCell.backgroundColor = [UIColor blueColor]; // highlight selection
 }  

 -(void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath {

UICollectionViewCell *datasetCell =[collectionView cellForItemAtIndexPath:indexPath]; 
datasetCell.backgroundColor = [UIColor redColor]; // Default color
}

Solution 2 - Ios

Framework will handle switching the views for you once you setup your cell's backgroundView and selectedBackgroundView, see example from Managing the Visual State for Selections and Highlights:

UIView* backgroundView = [[UIView alloc] initWithFrame:self.bounds];
backgroundView.backgroundColor = [UIColor redColor];
self.backgroundView = backgroundView;

UIView* selectedBGView = [[UIView alloc] initWithFrame:self.bounds];
selectedBGView.backgroundColor = [UIColor whiteColor];
self.selectedBackgroundView = selectedBGView;

you only need in your class that implements UICollectionViewDelegate enable cells to be highlighted and selected like this:

- (BOOL)collectionView:(UICollectionView *)collectionView
        shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath
{
    return YES;
}

- (BOOL)collectionView:(UICollectionView *)collectionView
        shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath;
{
    return YES;
}

This works me.

Solution 3 - Ios

UICollectionView has changed in iOS 10 introducing some problems to solutions above.

Here is a good guide: https://littlebitesofcocoa.com/241-uicollectionview-cell-pre-fetching

Cells now stay around for a bit after going off-screen. Which means that sometimes we might not be able to get hold of a cell in didDeselectItemAt indexPath in order to adjust it. It can then show up on screen un-updated and un-recycled. prepareForReuse does not help this corner case.

The easiest solution is disabling the new scrolling by setting isPrefetchingEnabled to false. With this, managing the cell's display with cellForItemAt didSelect didDeselect works as it used to.

However, if you'd rather keep the new smooth scrolling behaviour it's better to use willDisplay :

func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    let customCell = cell as! CustomCell
    if customCell.isSelected {
        customCell.select()
    } else {
        customCell.unselect()
    }
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath) as! CustomCell
    //Don't even need to set selection-specific things here as recycled cells will also go through willDisplay
    return cell
}

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    let cell = collectionView.cellForItem(at: indexPath) as? CustomCell
    cell?.select()
}

func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
    let cell = collectionView.cellForItem(at: indexPath) as? CustomCell
    cell?.unselect() // <----- this can be null here, and the cell can still come back on screen!
}

With the above you control the cell when it's selected, unselected on-screen, recycled, and just re-displayed.

Solution 4 - Ios

Anil was on the right track (his solution looks like it should work, I developed this solution independently of his). I still used the prepareForReuse: method to set the cell's selected to FALSE, then in the cellForItemAtIndexPath I check to see if the cell's index is in `collectionView.indexPathsForSelectedItems', if so, highlight it.

In the custom cell:

-(void)prepareForReuse {
    self.selected = FALSE;
}

In cellForItemAtIndexPath: to handle highlighting and dehighlighting reuse cells:

if ([collectionView.indexPathsForSelectedItems containsObject:indexPath]) {
    [collectionView selectItemAtIndexPath:indexPath animated:FALSE scrollPosition:UICollectionViewScrollPositionNone];
    // Select Cell
}
else {
    // Set cell to non-highlight
}

And then handle cell highlighting and dehighlighting in the didDeselectItemAtIndexPath: and didSelectItemAtIndexPath:

This works like a charm for me.

Solution 5 - Ios

I had a horizontal scrolling collection view (I use collection view in Tableview) and I too faced problems withcell reuse, whenever I select one item and scroll towards right, some other cells in the next visible set gets select automatically. Trying to solve this using any custom cell properties like "selected", highlighted etc didnt help me so I came up with the below solution and this worked for me.

Step1:

Create a variable in the collectionView to store the selected index, here I have used a class level variable called selectedIndex

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath

{

    MyCVCell *cell = (MyCVCell*)[collectionView dequeueReusableCellWithReuseIdentifier:@"MyCVCell" forIndexPath:indexPath];    

// When scrolling happens, set the selection status only if the index matches the selected Index

if (selectedIndex == indexPath.row) {

        cell.layer.borderWidth = 1.0;

        cell.layer.borderColor = [[UIColor redColor] CGColor];

    }
    else
    {
        // Turn off the selection
        cell.layer.borderWidth = 0.0;

    }
    return cell;

}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath

{
    MyCVCell *cell = (MyCVCell *)[collectionView cellForItemAtIndexPath:indexPath];
    // Set the index once user taps on a cell
    selectedIndex = indexPath.row;
    // Set the selection here so that selection of cell is shown to ur user immediately
    cell.layer.borderWidth = 1.0;
    cell.layer.borderColor = [[UIColor redColor] CGColor];
    [cell setNeedsDisplay];
}

- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath

{

    MyCVCell *cell = (MyCVCell *)[collectionView cellForItemAtIndexPath:indexPath];

    // Set the index to an invalid value so that the cells get deselected
    selectedIndex = -1;
    cell.layer.borderWidth = 0.0;
    [cell setNeedsDisplay];

}

-anoop

Solution 6 - Ios

What I did to solve this was to make the changes in the customized cell. You have a custom cell called DataSetCell in its class you could do the following (the code is in swift)

override var isSelected: Bool {
    didSet {
        if isSelected {
            changeStuff
        } else {
            changeOtherStuff
        }
    }
}

What this does is that every time the cell is selected, deselected, initialized or get called from the reusable queue, that code will run and the changes will be made. Hope this helps you.

Solution 7 - Ios

In your custom cell create public method:

- (void)showSelection:(BOOL)selection
{
    self.contentView.backgroundColor = selection ? [UIColor blueColor] : [UIColor white];
}

Also write redefenition of -prepareForReuse cell method:

- (void)prepareForReuse
{
    [self showSelection:NO];
    [super prepareForReuse];
}

And in your ViewController you should have _selectedIndexPath variable, which defined in -didSelectItemAtIndexPath and nullified in -didDeselectItemAtIndexPath

NSIndexPath *_selectedIndexPath;

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *cellIdentifier = @"Cell";
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];

    if (_selectedIndexPath) {
        [cell showSelection:[indexPath isEqual:_selectedIndexPath]];
    }
}

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
    [cell showSelection:![indexPath isEqual:_selectedIndexPath]];// on/off selection
    _selectedIndexPath = [indexPath isEqual:_selectedIndexPath] ? nil : indexPath;
}

- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
    [cell showSelection:NO];
    _selectedIndexPath = nil;
}

Solution 8 - Ios

Only @stefanB solution worked for me on iOS 9.3

Here what I have to change for Swift 2

func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        
        //prepare your cell here..
        
        //Add background view for normal cell
        let backgroundView: UIView = UIView(frame: cell!.bounds)
        backgroundView.backgroundColor = UIColor.lightGrayColor()
        cell!.backgroundView = backgroundView
        
        //Add background view for selected cell
        let selectedBGView: UIView = UIView(frame: cell!.bounds)
        selectedBGView.backgroundColor = UIColor.redColor()
        cell!.selectedBackgroundView = selectedBGView
        
        return cell!
 }

 func collectionView(collectionView: UICollectionView, shouldHighlightItemAtIndexPath indexPath: NSIndexPath) -> Bool {
        return true
 }
    
 func collectionView(collectionView: UICollectionView, shouldSelectItemAtIndexPath indexPath: NSIndexPath) -> Bool {
        return true
 }

Solution 9 - Ios

The problem you encounter comes from the lack of call to super.prepareForReuse().

Some other solutions above, suggesting to update the UI of the cell from the delegate's functions, are leading to a flawed design where the logic of the cell's behaviour is outside of its class. Furthermore, it's extra code that can be simply fixed by calling super.prepareForReuse(). For example :

class myCell: UICollectionViewCell {
	
	// defined in interface builder
	@IBOutlet weak var viewSelection : UIView!
	
	override var isSelected: Bool {
		didSet {
			self.viewSelection.alpha = isSelected ? 1 : 0
		}
	}
	
	override func prepareForReuse() {
		// Do whatever you want here, but don't forget this :
	    super.prepareForReuse()
		// You don't need to do `self.viewSelection.alpha = 0` here 
		// because `super.prepareForReuse()` will update the property `isSelected`
	
	}
	
	
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
		self.viewSelection.alpha = 0
    }

}

With such design, you can even leave the delegate's functions collectionView:didSelectItemAt:/collectionView:didDeselectItemAt: all empty, and the selection process will be totally handled, and behave properly with the cells recycling.

Solution 10 - Ios

you can just set the selectedBackgroundView of the cell to be backgroundColor=x.

Now any time you tap on cell his selected mode will change automatically and will couse to the background color to change to x.

Solution 11 - Ios

Thanks to your answer @[RDC][1].

The following codes works with Swift 3

// MARK: - UICollectionViewDataSource protocol
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    
    //prepare your cell here..
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath as IndexPath) as! MyCell
    cell.myLabel.text =  "my text"
    
    //Add background view for normal cell
    let backgroundView: UIView = UIView(frame: cell.bounds)
    backgroundView.backgroundColor = UIColor.lightGray
    cell.backgroundView = backgroundView
    
    //Add background view for selected cell
    let selectedBGView: UIView = UIView(frame: cell.bounds)
    selectedBGView.backgroundColor = UIColor.green
    cell.selectedBackgroundView = selectedBGView
    
    return cell
}

// MARK: - UICollectionViewDelegate protocol
func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool {
    return true
}

func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
    return true
}

[1]: https://stackoverflow.com/a/36859814/4965086 "RDC"

Solution 12 - Ios

Changing the cell property such as the cell's background colors shouldn't be done on the UICollectionViewController itself, it should be done inside you CollectionViewCell class. Don't use didSelect and didDeselect, just use this:

class MyCollectionViewCell: UICollectionViewCell 
{
     override var isSelected: Bool
     {
         didSet
         {
            // Your code
         }
     } 
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionPadin215View Question on Stackoverflow
Solution 1 - IosAnil VargheseView Answer on Stackoverflow
Solution 2 - IosstefanBView Answer on Stackoverflow
Solution 3 - IosAleksander NiedziolkoView Answer on Stackoverflow
Solution 4 - IosPadin215View Answer on Stackoverflow
Solution 5 - Iosanoop4realView Answer on Stackoverflow
Solution 6 - IosEliezer FerraView Answer on Stackoverflow
Solution 7 - IoslandonandreyView Answer on Stackoverflow
Solution 8 - IosswiftBoyView Answer on Stackoverflow
Solution 9 - IosPierre-Olivier SimonardView Answer on Stackoverflow
Solution 10 - IosevyaView Answer on Stackoverflow
Solution 11 - IosBenView Answer on Stackoverflow
Solution 12 - Iospierre23View Answer on Stackoverflow