50

So I have a main object that has many images associated with it. An Image is also an object.

Say you have a collection view controller, and in that controller you have

cellForItemAtIndexPath

well based on the main object, if it has the current image associated with it I want to set selected to true. But I want the user to be able to "un-select" that current cell at any time to remove its association with the main object.

I find that if you set "selected to true" - if there is an relation between the main object and image in cellForItemAtIndexPath, de-selection is no longer an option.

in

didDeselectItemAtIndexPath

and

didSelectItemAtIndexPath

I test with a log to see if they are called. If a cell is set to selected - nether gets called, but If I never set a cell to selected in cellForItemAtIndexPath I can select and deselect all I want.

Is this the intended way a collection view is supposed to work? I read the docs and it does not seem to talk about this being so. I interpret the docs to mean it works the way a table view cell would. with a few obvious changes

This also shows the controller is set up correct and is using the appropriate delegate methods.... hmmmm

Juan Boero
  • 5,451
  • 37
  • 56
bworby
  • 1,015
  • 1
  • 10
  • 17

15 Answers15

91

I had the same issue, ie. setting cell.selected = YES in [UICollectionView collectionView:cellForItemAtIndexPath:] then not being able to deselect the cell by tapping on it.

Solution for now: I call both [UICollectionViewCell setSelected:] and [UICollectionView selectItemAtIndexPath:animated:scrollPosition:] in [UICollectionView collectionView:cellForItemAtIndexPath:].

user2405793
  • 911
  • 6
  • 3
  • 2
    Strange that it should be necessary - but it works.. And it is not causing infinite loops with didSelectItemAtIntexPath delegate.. – Morten Holmgaard Jul 30 '13 at 09:58
  • 6
    Has anyone found a better solution since this was proposed? This solution also works for me, but this might be a bug. – itslittlejohn Feb 20 '14 at 20:32
  • 4
    You are a SAINT! A SAINT I TELL YOU! Not sure on the specific of the ScrollPosition enumeration but its working. (Y) – MrOli3000 Sep 05 '14 at 11:00
  • This is perhaps due to the internal way in which UICollectionView manages a reference on selections? It seems you need to call setSelected: to reflect on the view level too. Whatever, don't care, this works. – Adam Waite Sep 24 '14 at 13:53
  • 1
    This one really helped me out. Thanks!! – Ganesh Somani May 07 '15 at 09:24
  • 1
    Cheers to you! I think it was needing to call [UICollectionView selectItemAtIndexPath:animated:scrollPosition:]. Thanks! – Alex Sharp May 21 '15 at 20:13
  • After a lot of investigation, I've noticed that getting a reference to the cell to call cell.selected causes a reuse dequeue and calling selectItemAtIndexPath does not cause a dequeue. Triggering the dequeue to tell the cell it's selected can return a difference cell than the one drawn, so the cell knows it's selected, but the UI doesn't. The call to selectItemAtIndexPath gets the correct reference and updates the UI. – Chase Holland Nov 17 '15 at 19:09
  • Works like a magic... Was hitting my head on same issue from 1 day ....Finally It worked.. – Sakshi Dec 29 '15 at 17:41
  • Thank you very much. I have small experience with UICollectionView and you saved my time. – Alexander Balabanov Jun 22 '16 at 20:32
  • @user2405793 It is not working when I do scrolling in my collection view. last selected cell remains selected. – Pratik Oct 22 '19 at 18:04
  • This will create an issue if you don't wrap it with DispatchQueue.main.async {}. Also use .init() as the scrollPosition option if you don't want movements. – TDesign Apr 21 '20 at 23:16
48

I had a Deselect issue with UICollectionView and I found that I was not allowing multiple selection on collectionView. So when I was testing I tried always on the same cell and if single selection is ON you can't Deselect a cell already selected.

I had to add:

myCollectionView.allowsMultipleSelection = YES;
i_am_jorf
  • 51,120
  • 15
  • 123
  • 214
Fjohn
  • 1,582
  • 13
  • 10
  • 1
    this is the CORRECT answer – naz Sep 30 '14 at 10:27
  • I guess the reason most people miss that, is because it's not exposed in IB for collection views, right? I've looked for it, couldn't find it anywhere in IB. – Sagi Mann Dec 21 '16 at 12:12
46

Do you have a custom setSelected method in your Cell class? Are you calling [super setSelected:selected] in that method?

I had a mysterious problem where, I was using multiple selection, I could not deselect cells once they were selected. Calling the super method fixed the problem.

Aneel
  • 1,273
  • 1
  • 16
  • 22
44

This is kind of old but, since I encounter the same issue using swift I will add my answer. When using:

 collectionView.selectItemAtIndexPath(indexPath, animated: true, scrollPosition: [])

The cell didn't get selected at all. But when using:

cell.selected = true

It did get selected but I wasn't able to select/deselect the cell anymore.

My solution (use both methods):

cell.selected = true
collectionView.selectItemAtIndexPath(indexPath, animated: true, scrollPosition: .None)

When this two methods are called in:

func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell

It worked perfectly!

Kendall Helmstetter Gelner
  • 73,251
  • 26
  • 123
  • 148
dmlebron
  • 813
  • 6
  • 15
10

I don't know why UICollectionView is so messy like this compared to UITableViewController... A few things I found out.

The reason why setSelected: gets called multiple times is because of the sequence methods get called. The sequence is very similar to that of UITextFieldDelegate methods.

The method collectionView:shouldSelectItemAtIndexPath: is called before the collectionView actually selects the cell because it's actually asking "should it be selected"?

collectionView:didSelectItemAtIndexPath: is in fact called after the collectionView selects the cell. Hence the name "did select."

So this is what is happening in your case (and my case, and I had to wrestle hours over this).

A selected cell is touched again by the user to deselect. shouldSelectItemAtIndexPath: is called to check whether the cell should be selected. The collectionView selects the cell and then didSelectItemAtIndexPath is called. Whatever you do at this point is after the the cell's selected property is set to YES. That's why something like cell.selected = !cell.selected won't work.

TL;DR - Have your collectionView deselect the cell in the delegate method collectionView:shouldSelectItemAtIndexPath: by calling deselectItemAtIndexPath:animated: and return NO.

Short example of what I did:

- (BOOL)collectionView:(OPTXListView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath {
    NSArray *selectedItemIndexPaths = [collectionView indexPathsForSelectedItems];

    if ([selectedItemIndexPaths count]) {
        NSIndexPath *selectedIndexPath = selectedItemIndexPaths[0];

        if ([selectedIndexPath isEqual:indexPath]) {
            [collectionView deselectItemAtIndexPath:indexPath animated:YES];

            return NO;
        } else {
            [collectionView selectItemAtIndexPath:indexPath animated:YES scrollPosition:UICollectionViewScrollPositionCenteredHorizontally];

            return YES;
        }
    } else {
        [collectionView selectItemAtIndexPath:indexPath animated:YES scrollPosition:UICollectionViewScrollPositionCenteredHorizontally];

        return YES;
    }
}
funct7
  • 2,849
  • 2
  • 22
  • 29
7

Here is my answer for Swift 2.0.

I was able to set the following in viewDidLoad()

collectionView.allowsMultipleSelection = true;

then I implemented these methods

func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
    let cell = collectionView.cellForItemAtIndexPath(indexPath) as! MyCell
    cell.toggleSelected()
}

func collectionView(collectionView: UICollectionView, didDeselectItemAtIndexPath indexPath: NSIndexPath) {
    let cell = collectionView.cellForItemAtIndexPath(indexPath) as! MyCell
    cell.toggleSelected()
}

finally

class MyCell : UICollectionViewCell {

    ....

    func toggleSelected ()
    {
        if (selected){
            backgroundColor = UIColor.orangeColor()
        }else {
            backgroundColor = UIColor.whiteColor()
        }
    }

}
Trent
  • 1,892
  • 1
  • 23
  • 37
  • This works, but i need to change the border color of the selected cell, that doesn't work in the cell class. Can you suggest anything on that? – Arpit Dhamane Dec 20 '16 at 13:00
  • @ArpitDhamane This doesn't work for me either and I'm using Swift 3 Xcode 8.1. I can only change cell style inside "collectionView: UICollectionView, cellForItemAt indexPath:" – KMC Apr 22 '17 at 18:34
4

This question is six years old, but I don't care; after landing here and finding that none of the answers solved my specific problem, I eventually hit upon what I consider to be the best answer to this question. So, I'm posting it here for posterity.

Selecting cells in cellForItemAtIndexPath is problematic because of the way UICollectionView calls that method. It's not just called when initially setting up a collection view. It's called continuously as the collection view is scrolled, because the collection view only ever asks its data source for visible cells, thereby saving a lot of overhead.

Because collection views don't keep all of their cells in memory, they need to manage the selected state of their own cells. They don't expect you to provide them with cells whose isSelected property has been set. They expect you to provide them with cells, and they will set the selected property on them if appropriate.

This is why Apple cautions you to not set the isSelected property of UICollectionViewCell directly. UICollectionView expects to take care of that for you.

SO, the answer is to NOT attempt to select cells in the cellForItemAtIndexPath method. The best place to select cells that you want to be initially selected is in the viewWillAppear method of the UICollectionViewController. In that method, select all desired cells by calling UICollectionView.selectItem(at:animated:scrollPosition:), and DON'T set isSelected directly on your cells.

Ryan Ballantyne
  • 3,750
  • 3
  • 22
  • 26
2
 func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {

    let cell = collectionView.cellForItemAtIndexPath(indexPath)
    if cell?.selected == true{
        cell?.layer.borderWidth = 4.0
         cell?.layer.borderColor = UIColor.greenColor().CGColor
    }   
}func collectionView(collectionView: UICollectionView, didDeselectItemAtIndexPath indexPath: NSIndexPath) {
    let cell = collectionView.cellForItemAtIndexPath(indexPath)
    if cell?.selected == false{
            cell?.layer.borderColor = UIColor.clearColor().CGColor
    }

}

Simple Solution i found

Davin
  • 91
  • 1
  • 3
  • 9
2

Living in the age of iOS 9, there are multiple things to check here.

  1. Check do you have collectionView.allowsSelection set to YES
  2. Check do you have collectionView.allowsMultipleSelection set to YES (if you need that ability)

Now comes the fan part. If you listen to Apple and set backgroundColor on the cell.contentView instead of cell itself, then you have just hidden its selectedBackgroundView from ever being visible. Because:

(lldb) po cell.selectedBackgroundView
<UIView: 0x7fd2dae26bb0; frame = (0 0; 64 49.5); autoresize = W+H; layer = <CALayer: 0x7fd2dae26d20>>

(lldb) po cell.contentView
<UIView: 0x7fd2dae22690; frame = (0 0; 64 49.5); gestureRecognizers = <NSArray: 0x7fd2dae26500>; layer = <CALayer: 0x7fd2dae1aca0>>

(lldb) pviews cell
<MyCell: 0x7fd2dae1aa70; baseClass = UICollectionViewCell; frame = (0 0; 64 49.5); clipsToBounds = YES; hidden = YES; opaque = NO; layer = <CALayer: 0x7fd2dae1ac80>>
   | <UIView: 0x7fd2dae26bb0; frame = (0 0; 64 49.5); autoresize = W+H; layer = <CALayer: 0x7fd2dae26d20>>
   | <UIView: 0x7fd2dae22690; frame = (0 0; 64 49.5); gestureRecognizers = <NSArray: 0x7fd2dae26500>; layer = <CALayer: 0x7fd2dae1aca0>>
   |    | <UIView: 0x7fd2dae24a60; frame = (0 0; 64 49.5); clipsToBounds = YES; alpha = 0; autoresize = RM+BM; userInteractionEnabled = NO; layer = <CALayer: 0x7fd2dae1acc0>>
   |    | <UILabel: 0x7fd2dae24bd0; frame = (0 0; 64 17.5); text = '1'; opaque = NO; autoresize = RM+BM; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x7fd2dae240c0>>
   |    | <UILabel: 0x7fd2dae25030; frame = (0 21.5; 64 24); text = '1,04'; opaque = NO; autoresize = RM+BM; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x7fd2dae25240>>

(lldb) po cell.contentView.backgroundColor
UIDeviceRGBColorSpace 0.4 0.4 0.4 1

So if you want to use selectedBackgroundView (which is the one being turned on/off with cell.selected and selectItemAtIndexPath...) then do this:

cell.backgroundColor = SOME_COLOR;
cell.contentView.backgroundColor = [UIColor clearColor];

and it should work just fine.

Aleksandar Vacić
  • 4,304
  • 33
  • 33
  • 1
    Where do you get `pviews` from? I get an `error: Unrecognized command 'pview'.` on Xcode 7.2.1, lldb-340.4.119.1 – James Perih Apr 25 '16 at 18:15
  • 2
    `pviews cell` is simply a shortcut for `po [cell recursiveDescription]` call, which you can do on any view in lldb console – Aleksandar Vacić Apr 26 '16 at 07:31
  • correct. in order to use selected background view on collection view cell, only set color on cell background color, not the content view. – Yllow Mar 16 '17 at 08:40
1

I don't know that I understand the problem, but selected status is set per cell and would include all subviews within the cell. You don't explain what you mean by "a main object has many images associated with it." Associated as in subviews? Or what kind of association exactly do you mean?

It sounds like a design problem to me. Perhaps you need a UIView subclass that contains whatever associated objects you need to have; that subclass can then be set as the content view. I do this, for example, where I have an image, a description, and a sound recording related to the image. All are defined in the subclass and then each of these subclasses becomes a content view for a single cell.

I've also used an arrangement to related images to a folder which contains them. Under this set up, folders and images each have a subclass and either one might be attached to a cell as a content view (these are stored in core data as a single entity).

Perhaps you can further explain your problem?

RegularExpression
  • 3,473
  • 2
  • 23
  • 34
  • 1
    has anyone used the collection view, set selected to YES in cellForItemAtIndexPath and was able to de-select that cell? I have set up two test projects and both work the same. As soon as you set selected to true in cellForItemAtIndexPath deselect is never called again. – bworby Mar 12 '13 at 04:46
  • No, I don't think you can do that. If I set a cell to selected in celLForItemAtIndexPath, the item(s) behave as selected from the initial display of the collection view (i.e., selected background); however, if I look at the value of [collectionView indexPathsForSelectedItems], I get nil -- which suggests to me there would not be a way of deselecting it through the usual means. You may be able to set the value of cell.selected == NO, as I didn't try that. But if you can't get the index path of selected items, that is definitely going to make the collection view harder to work with. – RegularExpression Mar 12 '13 at 05:03
  • does this not seem odd? I may be thinking about it wrong but I could definitely see a ton of uses for allowing certain cells to start as selected, and be able to be deselected. – bworby Mar 13 '13 at 21:55
  • It does seem odd, but there may be some reason for it. Have you tried using selectItemAtIndexPath: ? – RegularExpression Mar 14 '13 at 03:49
  • Yes, I can get it to work that way or highlighting a cell and not selecting it. But then the fist touch does nothing (because that is the initial selection) and then it will deselect and re-select. – bworby Mar 15 '13 at 04:46
  • Yes and that does not solve it, I get the same result. I can get it to work by highlighting a cell and not selecting it. But then the fist touch does nothing (because that is the initial selection so it highlights it, which it already is) and then it will deselect and re-select for each additional tap. I figured I could allow certain images selected to start, and they could then be deSelected if desired by user. I have tried a number of "tricks" but really nothing works unless I completely override all delegate methods but I really dont think thats a valid solution. – bworby Mar 15 '13 at 04:53
  • Sorry I had to add 2 comments because I went past the 5 min deadline to edit when I added the first on accident. my apologies – bworby Mar 15 '13 at 04:54
1

Have you looked at:

- (BOOL)collectionView:(PSTCollectionView *)collectionView shouldDeselectItemAtIndexPath:(NSIndexPath *)indexPath;

Please state your problem more clearly and perhaps we can get to the bottom of it. I've just spent some time with UICollectionView.

I think your problem may be stemming from the confusion that if you set cell.selected = YES programmatically, the reason didSelectItemAtIndexPath: is not getting called is because that is only used when the collectionView itself is responsible for the cell's selection (eg. via a tap).

cleverbit
  • 4,995
  • 4
  • 24
  • 35
1

When calling both [UICollectionViewCell setSelected:] and [UICollectionView selectItemAtIndexPath:animated:scrollPosition:] in [UICollectionView collectionView:cellForItemAtIndexPath:] doesn't work try calling them inside a dispatch_async(dispatch_get_main_queue(), ^{}); block.

That's what finally fixed it for me.

Fran Sevillano
  • 7,893
  • 4
  • 28
  • 43
1

I'm using a custom cell subclass, for me I just had to set self.selected = false in prepareForReuse() in the subclass.

Chris
  • 915
  • 13
  • 20
0

Collectionview select, deselect issue solved, when I did this. In viewDidLoad, add collViewRiskPreferences.allowsMultipleSelection = false and add the following

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier:"MyCell", for: indexPath) as? MyCell else { return UICollectionViewCell() }
        
        cell.setupCellStyle(isSelected: false)
        
        return cell
    }

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        guard let cell = collectionView.cellForItem(at: indexPath) else { return }
        cell.setupCellStyle(isSelected: true)
    }
    
    func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
        guard let cell = collectionView.cellForItem(at: indexPath) else { return }
        cell.setupCellStyle(isSelected: false)
    }

and

class MyCell: UICollectionViewCell {
        func setupCellStyle(isSelected: Bool) {
            if(isSelected) {
                self.contentView.backgroundColor = UIColor.blue
            } else {
                self.contentView.backgroundColor = UIColor.green
            }
        }
    }
Sravan
  • 1,489
  • 3
  • 18
  • 28
-1

Cell selection and deselection is best handled by setting a backgroundView and a selected background view. I recommend making sure that both of these views' frames are set correctly in the layoutSubviews method (if you set the selected and background view via IB).

Don't forget to set your contentView's (if you have one) background color to clear so the correct background view shows through.

Never set the cell's selection directly (i.e. via cell.selected = YES), use the methods designed for this purpose in the collection view. It is clearly explained in the documents, although I will agree the information is somewhat fragmented across guides.

You should not need to poke into a cell's background colors directly in your collectionView datasource.

Also, as a final note, don't forget to call [super prepareForReuse] and [super setSelected:selected] if you are implementing these in your cell's class, as you might be preventing the cell's superclass from doing the cell selection.

Hit me up if you need further clarification on this subject.

kinora
  • 19
  • 3