Fun with UICollectionView

Episode #46 | 24 minutes | published on December 20, 2012
Subscribers Only
In this episode we dive into UICollectionView for displaying ... collections of views. We start by looking at how to tweak the builtin UICollectionViewFlowLayout as well as extending to create an interesting custom variation.

Episode Links

Creating a new UICollectionView

The easiest way to create a new collection view is to use Interface Builder. Just drag a collection view onto your surface and wire up the outlet property.

You'll also want to make your view controller the dataSource and delegate, and in order to do that, you'll have to conform to a couple of protocols.

@interface ViewController : UIViewController <UICollectionViewDataSource, UICollectionViewDelegate>
...
@end 

Then you need to implement the requisite datasource methods, similar to UITableView.

- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
    return 1;
}

- (NSInteger)collectionView:(UICollectionView *)collectionView 
     numberOfItemsInSection:(NSInteger)section {
    return 100;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
  // we'll come back to this in a second
}

The built-in UICollectionViewCell doesn't contain any UI (unlike UITableViewCell), so we'll have to subclass it to get anything displayed on screen.

Creating a custom collection view cell

Create a new class called NumberedCell.

// NumberCell.h
@interface NumberCell : UICollectionViewCell
@property (nonatomic, strong) UILabel *label;
- (void)setNumber:(NSInteger)number;
@end
// NumberCell.m
#import "NumberCell.h"
@implementation NumberCell
- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        self.label = [[UILabel alloc] initWithFrame:self.bounds];
        self.autoresizesSubviews = YES;
        self.label.autoresizingMask = (UIViewAutoresizingFlexibleWidth |
                                        UIViewAutoresizingFlexibleHeight);
        self.label.font = [UIFont boldSystemFontOfSize:42];
        self.label.textAlignment = NSTextAlignmentCenter;
        self.label.adjustsFontSizeToFitWidth = YES;

        [self addSubview:self.label];

        [self setNumber:0];
    }
    return self;
}

- (void)setNumber:(NSInteger)number {
    self.label.text = [NSString stringWithFormat:@"%d", number];
}
@end

Now, back in our view controller, we need to register this class to be used.

// at the top
#import "NumberCell.h"

- (void)viewDidLoad {
  // ...
  [self.collectionView registerClass:[NumberCell class] forReuseIdentifier:@"cell"];
}

Once the cell's class is registered, we can just dequeue it and it will always return a valid object.

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    NumberCell *cell = (NumberCell *)[collectionView dequeueReusableCellWithReuseIdentifier:@"cell"
                                                                               forIndexPath:indexPath];
    [cell setNumber:indexPath.row];
    return cell;
}

Now you can build & run the solution, and you should see the basic collection view layout.

Customizing the Layout

You can customize the built-in flow layout by setting a few properties.

UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init];
flowLayout.itemSize = CGSizeMake(75, 75);
flowLayout.minimumInterItemSpacing = 1;

self.collectionView.layout = flowLayout;

If you build and run now, you'll see a different layout, honoring these changed settings.

Creating a Wave Layout

We can customize this even further, but we'll have to subclass. We'll start by creating a new class called WaveLayout.

// WaveLayout.h
@interface WaveLayout : UICollectionViewFlowLayout

@end
// WaveLayout.m
#import "WaveLayout.h"
@implementation WaveLayout

// a change to do initialization or pre-determined layout for cells
- (void)prepareLayout {
    self.itemSize = CGSizeMake(75, 500);
    self.minimumInteritemSpacing = 30;
    self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
}

// called continuously as the rect changes
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    NSArray *attribs = [super layoutAttributesForElementsInRect:rect];

    CGRect visibleRect;
    visibleRect.origin = self.collectionView.contentOffset;
    visibleRect.size = self.collectionView.bounds.size;

    for (UICollectionViewLayoutAttributes *attributes in attribs) {
        if (CGRectIntersectsRect(attributes.frame, rect)) {
            CGFloat distanceFromCenter = CGRectGetMidX(visibleRect) - attributes.center.x;
            CGFloat normalizedDistance = distanceFromCenter / 100.0f;
            CGRect rect = attributes.frame;
            rect.origin.y = sinf(normalizedDistance) * 100.0f + 150.0f;
            attributes.frame = rect;
        }
    }

    return attribs;
}

// indicate that we want to redraw as we scroll
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
    return YES;
}
@end

Now if you use the WaveLayout in your class, by setting self.collectionView.layout = [[WaveLayout alloc] init]. Run the example now, and scroll around see what we've created!

UICollectionView is really complex and powerful, and we've just scratched the surface here. We will likely visit some more advanced concepts in a future episode.

blog comments powered by Disqus