Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I alternate a UICollectiveViewCell's background color based on row?

I know this is fairly easy for UITableViewCells but I'm not sure how to approach this using a UICollectionView.

EDIT. Pictures for clarification. Text content of the cells are not the same here but they should be. In landscape:

In portrait:

enter image description here

I tried to naively switch the color of my cell's text label with the cell's background color based on the index path's row property in the cellForItemAtIndexPath: method. However Index Path's row property isn't really a row in UICollectionViewFlowLayout.

like image 906
Steve Moser Avatar asked Aug 19 '13 15:08

Steve Moser


2 Answers

Neither the collection view nor its layout will tell you a “row number” for items. You have to compute it yourself.

If all of your measurements are uniform (which is the case if you didn't implement any UICollectionViewDelegateFlowLayout methods), you can compute it pretty easily.

There are a few places you could do the computation and assign the color. I'm going to show you the “proper” place to do it: in the layout manager.

The “Knowing When to Subclass the Flow Layout” section of the Collection View Programming Guide for iOS tells you the basic things you need to do, but it's pretty bare bones, so I'll walk you through it.

Note: since layoutAttributes is a pretty cumbersome identifier, I usually use pose instead.

UICollectionViewLayoutAttributes subclass

First of all, we need a way to pass the background color from the layout manager to the cell. The right way to do that is by subclassing UICollectionViewLayoutAttributes. The interface just adds one property:

@interface MyLayoutAttributes : UICollectionViewLayoutAttributes

@property (nonatomic, strong) UIColor *backgroundColor;

@end

The implementation needs to implement the NSCopying protocol:

@implementation MyLayoutAttributes

- (instancetype)copyWithZone:(NSZone *)zone {
    MyLayoutAttributes *copy = [super copyWithZone:zone];
    copy.backgroundColor = self.backgroundColor;
    return copy;
}

@end

UICollectionViewCell subclass

Next, the cell needs to use that backgroundColor attributes. You probably already have a UICollectionViewCell subclass. You need to implement applyLayoutAttributes: in your subclass, like this:

@implementation MyCell

- (void)applyLayoutAttributes:(MyLayoutAttributes *)pose {
    [super applyLayoutAttributes:pose];
    if (!self.backgroundView) {
        self.backgroundView = [[UIView alloc] init];
    }
    self.backgroundView.backgroundColor = pose.backgroundColor;
}

@end

UICollectionViewFlowLayout subclass

Now you need to make a subclass of UICollectionViewFlowLayout that uses MyLayoutAttributes instead of UICollectionViewLayoutAttributes, and sets the backgroundColor of each MyLayoutAttributes correctly. Let's define the interface to have a property which is the array of colors to assign to rows:

@interface MyLayout : UICollectionViewFlowLayout

@property (nonatomic, copy) NSArray *rowColors;

@end

The first thing we need to do in our implementation is specify what layout attributes class we want it to use:

@implementation MyLayout

+ (Class)layoutAttributesClass {
    return [MyLayoutAttributes class];
}

Next, we need to override two methods of UICollectionViewLayout to set the backgroundColor property of each pose. We call on super to get the set of poses, and then use a helper method to set the background colors:

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    NSArray *poses = [super layoutAttributesForElementsInRect:rect];
    [self assignBackgroundColorsToPoses:poses];
    return poses;
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
    MyLayoutAttributes *pose = (MyLayoutAttributes *)[super layoutAttributesForItemAtIndexPath:indexPath];
    [self assignBackgroundColorsToPoses:@[ pose ]];
    return pose;
}

Here's the method that actually assigns the background colors:

- (void)assignBackgroundColorsToPoses:(NSArray *)poses {
    NSArray *rowColors = self.rowColors;
    int rowColorsCount = rowColors.count;
    if (rowColorsCount == 0)
        return;

    UIEdgeInsets insets = self.sectionInset;
    CGFloat lineSpacing = self.minimumLineSpacing;
    CGFloat rowHeight = self.itemSize.height + lineSpacing;
    for (MyLayoutAttributes *pose in poses) {
        CGFloat y = pose.frame.origin.y;
        NSInteger section = pose.indexPath.section;
        y -= section * (insets.top + insets.bottom) + insets.top;
        y += section * lineSpacing; // Fudge: assume each prior section had at least one cell
        int row = floorf(y / rowHeight);
        pose.backgroundColor = rowColors[row % rowColorsCount];
    }
}

Note that this method makes several assumptions:

  • It assumes the scroll direction is vertical.
  • It assumes all items are the same size.
  • It assumes all sections have the same insets and line spacings.
  • It assumes every section has at least one item.

The reason for the last assumption is that all rows in a section are followed by the minimum line spacing, except for the last row, which is followed by the bottom section inset. I need to know how many rows preceded the current item's row. I try to do that by dividing the Y coordinate by the height of a row… but the last row of each section is shorter than the others. Hence the fudging.

Applying Your New Classes

Anyway, now we need to put these new classes to use.

First, we need to use MyLayout instead of UICollectionViewFlowLayout. If you're setting up your collection view in code, just create an instead of MyLayout and assign it to the collection view. If you're setting things up in a storyboard, find the collection view's layout (it is in the storyboard outliner as a child of the collection view) and set its custom class to MyLayout.

Second, we need to assign an array of colors to the layout. If you're using a storyboard, you probably want to do this in viewDidLoad. You could ask the collection view for its layout and then cast it to MyLayout. I prefer to use an outlet of the proper type, connected to the layout object in the storyboard.

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.layout.rowColors = @[
        [UIColor lightGrayColor],
        [UIColor cyanColor],
    ];
}

Result

If you got everything set up correctly, you'll get a result like this:

portrait screen shot

and in landscape orientation it looks like this:

landscape screen shot

I've put my test project in this github repository.

like image 57
rob mayoff Avatar answered Oct 11 '22 04:10

rob mayoff


I don' think there's any good general way to do this without subclassing the flow layout. For a more specific case, you can use integer math combined with the modulus operator to get "rows" from the indexPath. So, if you had 5 items on each row, you could return either 0 or 1 from this expression:

NSInteger rowTest = (indexPath.row / 5) % 2;

Just test this value, and change your color accordingly. Of course, you'll have to change the expression on rotation since you'll have a different number of items on each row.

like image 1
rdelmar Avatar answered Oct 11 '22 04:10

rdelmar