30

I have a custom UICollectionViewCell subclass that overwrites initWithFrame: and layoutSubviews to setup its views. However, I'm now trying to do two things which I'm having trouble with.

1) I'm trying to customize the state of the UICollectionViewCell upon selection. For example, I want to change one of the images in an UIImageView in the UICollectionViewCell.

2) I want to animate (light bounce) the UIImage in the UICollectionViewCell.

Can anyone point me in the right direction?

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
    MyCollectionViewCell *cell = (MyCollectionViewCell *)[collectionView cellForItemAtIndexPath:indexPath];
    [cell setSelected:YES];
}

6 Answers6

73

In your custom UICollectionViewCell subclass, you can implement didSet on the isSelected property.

Swift 3:

override var isSelected: Bool {
    didSet {
        if isSelected {
            // animate selection
        } else {
            // animate deselection
        }
    }
}

Swift 2:

override var selected: Bool {
    didSet {
        if self.selected {
            // animate selection
        } else {
            // animate deselection
        }
    }
}
Mike Sprague
  • 3,567
  • 1
  • 22
  • 25
  • How do you differentiate between the two states? I keep executing the `if` block when I tap on the cell. – Isuru Jan 28 '16 at 12:36
  • 1
    @Isuru if you tap the same cell over and over it will set it to true every time. Try tapping another cell. – Sean Clark Hess May 09 '16 at 20:45
  • 6
    This is not the recommended approach. It can (and does) cause a lot of problems, particularly when you're dealing with the selection and highlighting of groups of cells. As Apple's documentation says, it's better that you respond to selection and highlighting in the UICollectionView subclass, NOT the cell. That way, your UICollectionViewCell is responsible for maintaining and directing state; not the cell. Thus, you should respond to 'collectionView(_ collectionView:, didSelectItemAt indexPath:)', get the cell (or collectionView.visibleCells) and set the states and animations there. Cheers. – Womble Jul 15 '17 at 04:05
  • This is unstable. The isSelected "didSet" won't get called under certain circumstances. (Try scrolling a collection view and selecting cells very fast, at some point it doesnt get called) – Pochi May 08 '20 at 11:46
61

In your custom UICollectionViewCell subclass you could override the setSelected: as so:

- (void)setSelected:(BOOL)selected {
    [super setSelected:selected];

    if (selected) {
        [self animateSelection];
    } else {
        [self animateDeselection];
    }
}

I have found that on repeated touches this method is called on a cell even if it's already selected so you may want to just check that you are really changing state before firing unwanted animations.

thomh
  • 799
  • 1
  • 5
  • 9
  • 1
    I prefer this over the accepted answer, as this function is already built in, so you don't even have to override didSelectItemAtIndexPath if you don't want to. – WallMobile Mar 02 '13 at 23:42
  • 1
    That's about right, but you probably wouldn't want to roll your own animation blocks inside `setSelected` because the transition to/from selected state sometimes should not be animated (for example when the cell that was used for selected item is reused for an unselected one). It's better to rely on the animations provided by the collection view itself for `(de)selectItemAtIndexPath:animated:` calls with `animated == YES`. – Vadim Yelagin Aug 07 '13 at 11:27
  • 8
    This is a wrong approach because on reload you'll get wrong animations. – pronebird Oct 15 '13 at 20:14
  • 1
    I like this, because the behavior belongs in my cell class, not in the controller class. I'm reusing the same cell class and the cell should behave the same regardless of which view it shows up in, even though each view has a different datasource / delegate / controller. Using the accepted answer, I'd need to implement the same thing in multiple places, or somehow have all the controllers inherit from a single abstract one that defines the behavior. – ArtOfWarfare Sep 18 '14 at 00:42
  • There is two different things in the question: state & animation. State can be fixed with this post. Animation can't because of collectionView reload. – Martin Nov 25 '15 at 11:19
  • @ArtOfWarfare is right - every time a cell is reused it will perform selection/deselection animation so this is not enough. But it can be fixed by simply checking the previous value of `selected` and the index path of the cell. If the previous value of `selected` is different and there is an index path then you can animate because the cell is getting selected/deselected by some action. Otherwise the cell is being reused so don't animate. – kacho Feb 26 '16 at 10:55
15

Add a public method performSelectionAnimations to the definition of MyCollectionViewCell that changes the desired UIImageView and performs the desired animation. Then call it from collectionView:didSelectItemAtIndexPath:.

So in MyCollectionViewCell.m:

- (void)performSelectionAnimations {
    // Swap the UIImageView
    ...

    // Light bounce animation
    ...
}

And in your UICollectionViewController:

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
    MyCollectionViewCell *cell = (MyCollectionViewCell *)[collectionView cellForItemAtIndexPath:indexPath];
    [cell performSelectionAnimations];
}

Notice I've taken out the call to [cell setSelected:YES], since that should already be taken care of by the UICollectionView. From the documentation:

The preferred way to select the cell and highlight it is to use the selection methods of the collection view object.

idmean
  • 14,540
  • 9
  • 54
  • 83
Jonathan
  • 1,075
  • 2
  • 10
  • 18
  • All of my `UIImage` swap stuff is done in a method called from `layoutSubviews`. Should I just call `layoutSubviews` from `performSelectionAnimations`? –  Dec 01 '12 at 09:51
  • The documentation says not to call `layoutSubviews` directly, but to use `setNeedsLayout` or `layoutIfNeeded` instead. Anyway, `layoutSubviews` sounds like the wrong place for the swap+animation tasks. The purpose of `layoutSubviews` is to position subviews when necessary, including the first time the cell is displayed. But you're not (re)positioning things, and I assume you only want these tasks performed when the cell is selected. – Jonathan Dec 01 '12 at 10:06
  • 9
    This solution is totally wrong. Al least because the cell objects are reused and you'll end up with wrong cells displayed as selected as you scroll. – Vadim Yelagin Aug 07 '13 at 11:16
  • 1
    This also doesn't work if you call `collectionView?.selectItemAtIndexPath` – Mazyod May 08 '16 at 05:03
  • No, this IS the correct approach. You can get the cell in question, testing for optionality as appropriate, and then tell it to perform animations or otherwise change its state. You can also query the collectionView for its 'visibleCells' property, and then change their states/animations. This also allows the UICollectionView to control the state of groups of cells at once. – Womble Jul 15 '17 at 04:07
2

If you want to show animation on selection then following method might helpful to you :

 - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
     NSLog(@"cell #%d was selected", indexPath.row);


     // animate the cell user tapped on
     UICollectionViewCell  *cell = [collectionView cellForItemAtIndexPath:indexPath];

     [UIView animateWithDuration:0.8
                           delay:0
                         options:(UIViewAnimationOptionAllowUserInteraction)
                      animations:^{
                          [cell setBackgroundColor:UIColorFromRGB(0x05668d)];
                      }
                      completion:^(BOOL finished){
                          [cell setBackgroundColor:[UIColor clearColor]];
                      }
      ];


 }
elp
  • 8,021
  • 7
  • 61
  • 120
Gaurav
  • 8,227
  • 4
  • 34
  • 55
1

Should not mess with state when overridden in this way:

override var isSelected: Bool {

    get {
        return super.isSelected
    }

    set {
        super.isSelected = newValue
        .
        .
        .
    }
}
Hardcoded
  • 11
  • 2
0

From iOS 14, you can override updateConfiguration(using:) and update the cell according to the UICellConfigurationState.isSelected

Sunil Chauhan
  • 2,074
  • 1
  • 15
  • 33