36

The first thing I do is to set the cell selected.

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
    cell.selected = YES;
    return cell;
}

And the cell is successfully selected. If the user touches the selected cell, than should the cell be deselected and the delegates be called. But this never happen.

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
    NSLog(@"%s", __PRETTY_FUNCTION__);
}

- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath {
    NSLog(@"%s", __PRETTY_FUNCTION__);
}

I know that the delegates are not called if I set the selection programmatically. The delegate and datasource are set.

However, this delegate gets called:

- (BOOL)collectionView:(UICollectionView *)collectionView shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath {
    NSLog(@"%s", __PRETTY_FUNCTION__);
    return YES;
}

If I remove the cell.selected = YES than everything is working. Is there any one who can me explain this behaviour?

zeiteisen
  • 7,078
  • 5
  • 50
  • 68
  • When you are using cellforItemAtIndexPath , it gets called for every cell in the tableview, so basically you are trying to set each and every cell selected. What are you trying to achieve ? – userx Dec 04 '13 at 17:16

8 Answers8

71

The issue is, that the cell is selected, but the UICollectionView does not know anything about it. An extra call for the UICollectionView solves the problem:

[collectionView selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionNone]; 

Full code:

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
    cell.selected = YES;
    [collectionView selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionNone];
    return cell;
}

I keep the question to help someone who may face the same problem.

zeiteisen
  • 7,078
  • 5
  • 50
  • 68
  • 7
    Hello @zeiteisen. In my case, when i select a cell `didSelectItemAtIndexPath` gets called but when second time in click on cell it doesn't call `didDselectItemAtIndexPath`. I want to change the content of cell in both methods.How can i do this? – Sushil Sharma Nov 27 '14 at 10:57
  • It works, but I yet have to understand why it's needed: the collection view calls back to say the user selected the cell -> I ask it to reload the cell so I can reflect the change visually, then when it asks for the cell, I need to tell it to select it (again)? – Guy Moreillon Jun 21 '17 at 14:37
27

I think the solution @zeiteisen provided is a way around solution. The actual thing lies in the selection mode. There is nothing to be set the in property cell.selected.

There are two properties of the UICollectionView name allowsMultipleSelection and allowsSelection.

allowsMultipleSelection is NO by default and allowsSelection is YES.

If you want to select only one cell then the code @the initialization look like

yourCollectionView.allowsMultipleSelection = NO;
yourCollectionView.allowsSelection = YES; //this is set by default

Then the settings will allow you select only one cell at a time, not less not more. You must have to select at least one cell. The didDeselectItemAtIndexPath wont be called unless you select another cell. Tapping an already selected UICollectionViewCell won't make it to Deselect. This is the policy behind the implementation.

If you want to select multiple cell then the code @the initialization should look like the following:

yourCollectionView.allowsMultipleSelection = YES;
yourCollectionView.allowsSelection = YES; //this is set by default

Then the settings will allow to select multiple cell. This settings allow none or multiple UICollectionViewCell to be selected. Number of cell selected will be

0 <= number_selected_cell <= total_number_of_cell

This is the policy behind the multiple cell selection.

If you are intended to use any of the above you are welcome to use apples api without extra headache, otherwise you got to find a way around to sneak into your goal using your own code or apple's api.

psci
  • 903
  • 9
  • 18
Ratul Sharker
  • 7,484
  • 4
  • 35
  • 44
  • You are right, to put it simple: the collection view will not automatically deselect the already selected cell if the user tap it again. What is stated in the document of allowsMultipleSelection property is WRONG. And to me, it is quite freak for apple to implement it like this, to implement the "tap again and deselect" behavior we should maintain a selection flag ourselves. – Eddie Deng Jan 24 '16 at 13:06
  • 1
    This should be selected as proper answer. The key is to set allowsMultipleSelection=YES. – Billy Aug 22 '17 at 03:41
  • I faced the problem @Chris mentioned. Apparently it was because of overriding setIsSelected in my custom collectionView cell and not calling super. Selection and deselection works as specified in documentation. – yeshu Jul 25 '19 at 10:19
7

I had the same issue. I pre-selected cells upon table load yet I had to double tap a cell to deselect it. @zeiteisen answer helped me. However, I'm working in Swift 3 so I thought I'd give an answer in Swift.

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell

    if /*Test for Selection*/ {
        cell.isSelected = true
        collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .left)
    }        
    return cell
}
Meagan S.
  • 974
  • 9
  • 11
2

I was having the same issue, if you investigate further you willl see that not only the diddiselect is not called, but the touch itself is not perceived by the cell. What solved for me was to allow for async execution of the selection and set the selecItem manually as well. Inside cellForItemAt.

       DispatchQueue.main.async {
            cell.isSelected = true
            collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .init())
        }

Use .init() and false on animated if you don't want to see movements. If you don't wrap this with the async block you will see the UI jumping while scrolling.

TDesign
  • 569
  • 4
  • 8
  • Thanks for the answer. Adding `collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .init())`to `cellForItemAt` solved the problem for me. But I think `cell.isSelected` is redundant. For details please see [isSelected | Apple Developer Documentation](https://developer.apple.com/documentation/uikit/uicollectionviewcell/1620130-isselected). – Nazım Gediz Aydındoğmuş Oct 11 '22 at 23:55
1

Add this line to your didSelectItemAtIndexPath method

[collectionView selectItemAtIndexPath:indexPath animated:NO scrollPosition:nil]
Nupur Sharma
  • 1,106
  • 13
  • 15
1

In my case, I want to change the background of the button in other words the background of the cell in the collection view:

class CustomCVCell: UICollectionViewCell {

override var isSelected: Bool {
        didSet {
            grayBackgroundViewWithImage.image =
                isSelected ? UIImage(named: "") : UIImage()
        }
    }

In the main class where the collection view is stored create this variable:

class CustomViewController: UIViewController {

///save the indexPath of last selected cell
private var lastSelectedIndexPath: IndexPath? }

In viewDidLoad() set this value to false:

customCollectionView.allowsMultipleSelection = false

Further code in data source. In my case, the first cell should be is selected:

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCVCell.cellID(),
                                                  for: indexPath) as! CustomCVCell
    
    if indexPath.row == 0 {
        lastSelectedIndexPath = indexPath
        cell.isSelected = true
    }
    
    //update last select state from lastSelectedIndexPath
    cell.isSelected = (lastSelectedIndexPath == indexPath)
    
    return cell
}

Further code in the delegate:

///UICollectionViewDelegate
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    guard lastSelectedIndexPath != indexPath else { return }

         if let index = lastSelectedIndexPath {
            let cell = collectionView.cellForItem(at: index) as! CustomCVCell
            cell.isSelected = false
          }

          let cell = collectionView.cellForItem(at: indexPath) as! CustomCVCell
          cell.isSelected = true
    lastSelectedIndexPath = indexPath
}

enter image description here enter image description here enter image description here enter image description here

PAULMAX
  • 87
  • 1
  • 11
  • Hi PAUL i'm using this solution for the selection of category, it's helping me to achive the solution, but some time i get this error "Fatal error: Unexpectedly found nil while unwrapping an Optional value" at the line "let cell = collectionView.cellForItem(at: index) as! CustomCVCell" Can you help me out to resolve the issue? Actully i'm not getting the exact issue. If I use this line if indexPath.row == 0 { lastSelectedIndexPath = indexPath cell.isSelected = true } My 1st index is always selected and I don't want to show it selected for 1st time – Rana Ali Waseem May 26 '22 at 10:53
  • 1
    Hi @RanaAliWaseem)) I hope you initialize the cell with id correctly and register it. Perhaps this line is redundant for your solution: ///update last select state from lastSelectedIndexPath cell.isSelected = (lastSelectedIndexPath == indexPath) – PAULMAX May 31 '22 at 20:04
0

Reloading the cell was solution for me. What i need was changing image in cell for a brief moment. I assign image to imageView in cellForItemAtIndexPath and when user touches the cell i change image and run mycode then reload the cell.

 func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
        let cell = collectionView.cellForItemAtIndexPath(indexPath) as! MenuCollectionViewCell
        cell.backgroundImageView.image = UIImage(named: "selected")
        // handle tap events
        collectionView.reloadItemsAtIndexPaths([indexPath])
    }

Hope it helps someone.

ysnzlcn
  • 558
  • 6
  • 18
0

In my case it was caused by using same logic for

func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool

as for

func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool

And when cell was selected shouldHighlightItemAt was returning false - and this prevented me to deselect that cell.

Hope this will save someone's time :)

mra214
  • 509
  • 1
  • 7
  • 18