35

According to the Collection View Programming Guide one should handle the visual state of the cell highlights in the UICollectionViewDelegate. Like this:

- (void)collectionView:(PSUICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath
{
    MYCollectionViewCell *cell = (MYCollectionViewCell*)[collectionView cellForItemAtIndexPath:indexPath];
    [cell highlight];
}

- (void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath
{
    MYCollectionViewCell *cell = (MYCollectionViewCell*)[collectionView cellForItemAtIndexPath:indexPath];
    [cell unhighlight];
}

What I don't like about this approach is that it adds logic to the delegate that is very specific to the cell. In fact, UICollectionViewCell manages its highlighted state independently, via the highlighted property.

Wouldn't overriding setHighlighted: be a cleaner solution, then?

- (void)setHighlighted:(BOOL)highlighted
{
    [super setHighlighted:highlighted];
    if (highlighted) {
        [self highlight];
    } else {
        [self unhighlight];
    }
}

Are there any disadvantages to this approach instead of the delegate approach?

hpique
  • 119,096
  • 131
  • 338
  • 476

7 Answers7

53

As the documentation says, you can rely on highlighted property to be changed while the cell is highlighted. For example the following code will make the cell red when highlighted (not its subviews though):

- (void)setHighlighted:(BOOL)highlighted {
    [super setHighlighted:highlighted];
    [self setNeedsDisplay];
}

- (void)drawRect:(CGRect)rect {
    [super drawRect:rect];

    if (self.highlighted) {
        CGContextRef context = UIGraphicsGetCurrentContext();
        CGContextSetRGBFillColor(context, 1, 0, 0, 1);
        CGContextFillRect(context, self.bounds);
    } 
}

And if you add something like this the background will become purple (red + opaque blue):

- (void)collectionView:(UICollectionView *)colView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewCell *cell = [colView cellForItemAtIndexPath:indexPath];
    cell.contentView.backgroundColor = [UIColor colorWithRed:0 green:0 blue:1 alpha:0.5];
}

- (void)collectionView:(UICollectionView *)colView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewCell *cell = [colView cellForItemAtIndexPath:indexPath];
    cell.contentView.backgroundColor = nil;
}

So you can use both together (not necessarily both changing the cell appearance). The difference is that with delegate methods you also have indexPath. It might be used to create multi-selection (you will use this methods together with selection delegate methods), to show some preview while the cell is highlighted, to show some animation with other views... There's quite a few appliance for this delegate methods in my opinion.

As a conclusion, I would leave the cell appearance to be handled by the cell itself and use delegate methods to let controller make something cool in the same time.

shim
  • 9,289
  • 12
  • 69
  • 108
A-Live
  • 8,904
  • 2
  • 39
  • 74
  • 2
    +1 Thanks. I like the `setNeedsDisplay` + `drawRect` approach. Maybe I would check if `self.highlighted != highlighted` (on line 1) before calling `setNeedsDisplay`, to avoid unnecessary drawing. – hpique Mar 18 '13 at 18:51
  • You don't need the ```drawRect``` and ```setNeedsDisplay``` calls when overriding ```setHighlighted:```. This will do: ```self.contentView.backgroundColor = highlighted ? UIColor(white: 217.0/255.0, alpha: 1.0) : nil``` – bizz84 Jan 14 '16 at 10:51
  • @bizz84 if you read the answer carefully that is explained there. – A-Live Jan 14 '16 at 11:46
  • beautiful explanation I was searching for this to be clarified! – StackGU Nov 24 '20 at 21:30
17

Two possible approaches are outlined below.

Cell Subclassing

Cleaner approach if already subclassing from UICollectionViewCell.

class CollectionViewCell: UICollectionViewCell {

    override var highlighted: Bool {
        didSet {
            self.contentView.backgroundColor = highlighted ? UIColor(white: 217.0/255.0, alpha: 1.0) : nil
        }
    }
}

UICollectionViewDelegate

Less clean, requires the collection view delegate to know about the presentation logic of the cells.

func collectionView(collectionView: UICollectionView, didHighlightItemAtIndexPath indexPath: NSIndexPath) {

    if let cell = collectionView.cellForItemAtIndexPath(indexPath) {
        cell.contentView.backgroundColor = UIColor(white: 217.0/255.0, alpha: 1.0) // Apple default cell highlight color
    }
}


func collectionView(collectionView: UICollectionView, didUnhighlightItemAtIndexPath indexPath: NSIndexPath) {

    if let cell = collectionView.cellForItemAtIndexPath(indexPath) {
        cell.contentView.backgroundColor = nil
    }
}
bizz84
  • 1,964
  • 21
  • 34
8

It is enough for highlighting cell (Swift 4)

class MyCollectionViewCell: UICollectionViewCell {
...
    override var isHighlighted: Bool {
        didSet {
            if isHighlighted {
                self.contentView.alpha = 0.6
            }
            else {
                self.contentView.alpha = 1.0
            }
        }
    }
}
Viktor
  • 1,155
  • 14
  • 23
7

Notice that UICollectionViewCell has a selectedBackgroundView property. By default, it's nil. Just create a view for this property, and it will appear when the user touches the cell.

override func awakeFromNib() {
    super.awakeFromNib()

    let view = UIView(frame: contentView.bounds)
    view.isUserInteractionEnabled = false
    view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    view.backgroundColor = UIColor(white: 0.94, alpha: 1.0)
    selectedBackgroundView = view
}
Dave Batton
  • 8,795
  • 1
  • 46
  • 50
6

Well...as all of these methods are correct. I've found the way that seems like the easiest one to me. Just override the setSelected: method (for example to change background color):

-(void)setSelected:(BOOL)selected{
    self.backgroundColor = selected?[UIColor greenColor]:[UIColor grayColor];
    [super setSelected:selected];
}

...it works "out of the box" (even with collectionView.allowsMultipleSelection)

JakubKnejzlik
  • 6,363
  • 3
  • 40
  • 41
  • If you have multiple cells and you scroll up and down, it selects random cells, is there a solution for that? – Munib Jul 20 '18 at 14:13
2

As taken directly from UICollectionViewCell.h - overriding both setSelected and setHighlighted are correct. Depending upon your situation you might consider assigning custom views to backgroundView and selectedBackgroundView which are swapped automatically on selection.

// Cells become highlighted when the user touches them.
// The selected state is toggled when the user lifts up from a highlighted cell.
// Override these methods to provide custom UI for a selected or highlighted state.
// The collection view may call the setters inside an animation block.
@property (nonatomic, getter=isSelected) BOOL selected;
@property (nonatomic, getter=isHighlighted) BOOL highlighted;

// The background view is a subview behind all other views.
// If selectedBackgroundView is different than backgroundView, it will be placed above the background view and animated in on selection.
@property (nonatomic, retain) UIView *backgroundView;
@property (nonatomic, retain) UIView *selectedBackgroundView;
Brody Robertson
  • 8,506
  • 2
  • 47
  • 42
0

Swift 3: (based on the answer of A-Live)

import UIKit

class MyCollectionViewCell: UICollectionViewCell {

    override var highlighted: Bool {
        didSet {
            self.setNeedsDisplay()
        }
    }

    override func drawRect(rect: CGRect) {
        super.drawRect(rect)
        myImageView.highlighted = self.highlighted
    }
}

Swift 4

import UIKit

class MyCollectionViewCell: UICollectionViewCell {

    override var isHighlighted: Bool {
        didSet {
            self.setNeedsDisplay()
        }
    }

    override func draw(_ rect: CGRect) {
        super.draw(rect)
        myImageView.isHighlighted = self.isHighlighted
    }
}
andrewtweber
  • 24,520
  • 22
  • 88
  • 110
oOEric
  • 1,069
  • 1
  • 10
  • 25