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:
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.
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
subclassFirst 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
subclassNext, 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
subclassNow 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:
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.
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],
];
}
If you got everything set up correctly, you'll get a result like this:
and in landscape orientation it looks like this:
I've put my test project in this github repository.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With