Adoption Curve Dot Net

A Simple UICollectionView Tutorial

| Comments

UICollectionView operates in a very similar way to UITableView – it’s a collection of cells that are displayed onscreen according to the properties defined in a layout. The cells are provided by a class that acts as the collection view’s data source; and the collection view relies on a delegate to tell it how to behave in response to user interactions and so on.

This is a quick tutorial that covers the basics of UICollectionView – you can download the source code from GitHub at https://github.com/timd/CollectionViewExample.git.

There are three main components to a UICollectionView:

  • the cells, which are instances of UICollectionViewCell

  • supplementary views, which you can think of as views that can display additional metadata about the sections of the UICollectionView

  • decoration views, which provide the ‘chrome’ for the UICollectionView

UICollectionView operates in a very similar way to UITableView. It’s a collection of instances of UICollectionViewCell that are laid out according to the properties defined in an instance of UICollectionViewLayout:

UICollectionViewCell

This is equivalent to UITableViewCell – it has an indexPath property which defines which row and section it belongs to; and various other properties to define the visual appearance. Unlike UITableViewCell, UICollectionViewCell doesn’t have any predefined types – you have to setup the cell from scratch

UICollectionViewLayout

This class controls how the cells are laid out visually, controlling aspects like position, opacity and z index. There’s no direct equivalent in the UITableView world, though – it’s more like a mix of both table and cell properties. UICollectionViewFlowLayout is a predefined subclass of UICollectionViewLayout that is setup for line-based flow layouts – a vanilla instance of UICollectionViewFLowLayout will create a simple grid view, which you can then customise further by overriding properties or subclassing completely.

UICollectionViewDataSource

Much like UITableViewDataSource, the UICollectionViewDataSource is responsible for providing cells to the collection view on demand. It’s defined by the UICollectionViewDataSource protocol – there are several required methods, and a whole range of optional ones.

UICollectionViewDelegate

UICollectionViewDelegate works in much the same way as UITableViewDelegate – it’s responsible for handling user interaction amongst other things, and is defined by the UICollectionViewDelegate protocol.

Creating a UICollectionView

Here’s a worked example of putting a simple flow layout-based collection together from scratch. It’ll show a scrollable grid of cells which have a custom property – we’ll look at the three ways of building custom UICollectionViewCells as we go along.

Creating the basic app

First off, create a new Single View application:

  • File > New Project > Single View Application Name and save the project
  • Don’t tick ‘Use Storyboards’ or ‘Include Unit Tests’
  • Select ‘iPad’ as the device type

This will create a simple outline app, with an app delegate and a single view controller. If you run the app now, it’ll start with a full-screen blank view.

To being the process of building the collection view, first switch to the ViewController.h file, and add the UICollectionViewDelegate and UICollectionViewDatasource protocols:

@interface ViewController : UIViewController

Then switch to the ViewController.xib file. From the Object Browser, drag a UICollectionView object into the editor, and let it snap to fill the full view:

Next, add an IBOutlet property for the collection view to the view controller. Switch to ViewController.m, and add a property for the collection view:

@interface ViewController ()
@property (nonatomic, strong) IBOutlet UICollectionView *collectionView;
@end

Then connect the UICollectionView object in the nib to the outlet. Switch back to ViewController.xib, ctrl-click on the File's Owner object in the Placeholders list top left, then drag out to the UICollectionView object in the view. Release the mouse button, then select collectionView from the popup.

Now connect the UICollectionView object to the dataSource and delegate. Ctrl-click on the UICollectionView object, drag out to the File's Ownerobject, then select dataSource from the popup when you release the mouse button. Repeat the same process to connect the delegate.

That’s got the objects wired up. The next stage is to provide some data for the UICollectionView.

To keep things simple, I’m going to declare an NSArray property for the data:

@property (nonatomic, strong) NSArray *dataArray;

and then fill that with two NSMutableArrays, one for each section, that contain 50 NSStrings. This is done in the viewDidLoad method:

NSMutableArray *firstSection = [[NSMutableArray alloc] init]; NSMutableArray *secondSection = [[NSMutableArray alloc] init];
for (int i=0; i<50; i++) {
    [firstSection addObject:[NSString stringWithFormat:@"Cell %d", i]];
    [secondSection addObject:[NSString stringWithFormat:@"item %d", i]];
}
self.dataArray = [[NSArray alloc] initWithObjects:firstSection, secondSection, nil];

Having created some data, the next thing we need to do is to tell the collection view how many sections it will have. This means implementing the optional numberOfSectionsInCollectionView: method:

-(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
    return [self.dataArray count];
}

Once the collection view knows how many sections it has, we’ll need to provide the number of items in each section, with the collectionView:numberOfItemsInSection: method:

-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    NSMutableArray *sectionArray = [self.dataArray objectAtIndex:section];
return [sectionArray count]; }

At this point, the collection view knows how many sections it has, and how many items will be present in each section. The next step is to create the cells for display.

CREATING UICOLLECTIONVIEW CELLS

UITableViewCell comes with a range of “standardised” cell layouts that you can use – UICollectionViewCell isn’t quite as sophisticated, however, so you need to create your own. The process is very similar to creating table cells – you can create a cell in a standalone xib; or subclass a UICollectionViewCell. We’ll look at both approaches, starting with the xib-based method.

To begin with, create a new view (File > New > File) and select the View option from the User Interface section. It doesn’t matter which device family you choose, so give it a name (I’m calling mine NibCell). It will open up in the canvas – the first job is to delete the existing view, and drag in a Collection View Cell from the Object Browser.

Resize this so it’s 200 x 200, change the background color and add in a label so that it looks like this:

Set the tag value of the UILabel to 100 – we’ll use this later.

Having created the xib, it’s time to wire it up to the collection view. At the same time, we can also apply a UICollectionViewLayout to the collection view, as we now know what size the cells should be. Head back to the view controller, and update the viewDidLoad method with these lines:

UINib *cellNib = [UINib nibWithNibName:@"NibCell" bundle:nil];
[self.collectionView registerNib:cellNib forCellWithReuseIdentifier:@"cvCell"];

This grabs the nib from the NibCell file, and registers it with the collection view with a reuse identifier of “cvCell”. This will be used later to dequeue and instantiate a new cell ready for use.

A collection view is useless without a layout, so we need to create this. Add these four lines to the bottom of the viewDidLoad method:

UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init];
[flowLayout setItemSize:CGSizeMake(200, 200)];
[flowLayout setScrollDirection:UICollectionViewScrollDirectionHorizontal];

[self.collectionView setCollectionViewLayout:flowLayout];

This first of all creates a new instance of UICollectionViewFlowLayout, which is the ready-made flow-based layout that’s provided in the iOS SDK. It’s got various properties that can be configured, but we’re only going to set two – firstly the item size, which is the same as the UICollectionViewCell we created in the xib; and secondly the scroll direction (horizontal at the moment, although we’ll play around with that later). Finally, we add the configured layout to the collection view.

At this point, we’re ready to build the method that returns the cells to the collection view as they’re needed. This is the collectionView:cellForItemAtIndexPath: method, which is very similar to UITableView's tableView:cellForRowAtIndexPath: method. Add this to the view controller, and then we’ll step through it:

-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {

    NSMutableArray *data = [self.dataArray objectAtIndex:indexPath.section];

    NSString *cellData = [data objectAtIndex:indexPath.row];

    static NSString *cellIdentifier = @"cvCell";

    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];

    UILabel *titleLabel = (UILabel *)[cell viewWithTag:100];

    [titleLabel setText:cellData];

    return cell;

}

The first step is to get the data for the cell by grabbing the NSString from the cellData array (which is itself an element in the dataArray property). Next, we create a cellIdentifier reference, and then ask the collectionView to dequeue a reusable cell with that reuse identifier. If there is a reusable cell in the collectionView's cache, this will be returned to us ready for configuration – if not, the collectionView will create a new one for us behind the scenes. We neither know nor care which process takes place – as far as we’re concerned, dequeueReusableCellWithReuseIdentifier:forIndexPath: is magic and always returns a UICollectionViewCell ready for use.

Because we registered a nib with this cell identifier, the collectionView will have loaded and decompressed the xib file ready for use. We placed a UILabel inside the UICollectionViewCell and gave it tag of 100 – which means we can peer into the cell and grab a reference to the label (the reference is actually a UIView, so we need to cast it to a UILabel in order to be able to access the UILabel properties.)

Once the UILabel properties are accessible, they can be set, and the cell can be returned to be displayed by the collection view.

Once all that is in place, run the app, and you should see this:

CREATING UICOLLECTIONVIEW CELLS WITH SUBCLASSES

Creating a custom UICollectionViewCell subclass is an alternative to the purely nib-based approach, and can provide a greater degree of control over the appearance and behaviour of the cell.

The first thing we’ll need is a subclass of UICollectionViewCell. Create a new class (File > New > File) by selecting the Objective-C class option from the Cocoa Touch list, and then set the subclass to UICollectionViewCell. I’ve named my class CVCell.

This will create two files – a header and implementation for CVCell. In the header, create IBOutlet properties for any controls that you’re going to add to the xib file – I’ve added a single UILabel:

@property (nonatomic, strong) IBOutlet UILabel *titleLabel;

Then in the implementation file, you’ll need to override the initWithFrame: method:

- (id)initWithFrame:(CGRect)frame {

    self = [super initWithFrame:frame];

    if (self) {
        // Initialization code
        NSArray *arrayOfViews = [[NSBundle mainBundle] loadNibNamed:@"CVCell" owner:self options:nil];

        if ([arrayOfViews count] < 1) {
            return nil;
        }

        if (![[arrayOfViews objectAtIndex:0] isKindOfClass:[UICollectionViewCell class]]) {
            return nil;
        }

        self = [arrayOfViews objectAtIndex:0];

    }

    return self;

}

This firstly calls the superclass’s initWithFrame: method, then loads the xib file (which we’ll create in a second) into an NSArray. If the array is empty, then something has gone wrong and all we can do is return nil. Otherwise, we can grab a reference to the object at index 0 in the array. That object should be a UICollectionViewCell object – if isn’t, something has gone wrong and we bail out. It if is, we set it to self (because this object is itself an instance of UICollectionViewCell) and then return.

Once that class is created, we can build the cell using Interface Builder. As before, create a new view file, delete the view object that’s provided for you and drag in a UICollectionViewCell object. Then add a UILabel as we did previously.

The next job is to change the class of the UICollectionViewCell object to our new CVCell class. Select the Cell object in the Objects list and click the Identity inspector icon to reveal the Custom Class setting. At the moment, this will be set to UICollectionViewCell – delete that, and replace it with CVCell.

If you now ctrl-click on the Cell object in the Objects list, you’ll see that there’s a titleLabel listed as an outlet in the popup view. That’s the titleLabel property that we created in the CVCell class – you can drag from this onto the UILabel, and this will connect the two together. You’ll now be able to set the attributes of the UILabel through the CVCell class’s titleLabel property.

We’re done with the xib file, so save it and head back to the view controller.

As we’ve now got a custom UICollectionViewCell subclass, we need to import the subclass, and then register this with the collectionView. Add an import statement at the top of the view controller:

#import "CVCell.h"

At the moment, there are two lines in the viewDidLoad method that register the nib – remove these, and replace them with:

[self.collectionView registerClass:[CVCell class] forCellWithReuseIdentifier:@"cvCell"];

That’s pretty self-explanatory – the collectionView has the CVCell class registered for use with the cvCell reuse identifier, so whenever we ask the collectionView to dequeue a cell using this identifier, it will hand us back an instance of the CVCell class.

This means we need to update the collectionView:cellForItemAtIndexPath: method. Alter it so that it looks like this:

-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {

    static NSString *cellIdentifier = @"cvCell";

    CVCell *cell = (CVCell *)[collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];

    NSMutableArray *data = [self.dataArray objectAtIndex:indexPath.section];

    NSString *cellData = [data objectAtIndex:indexPath.row];

    [cell.titleLabel setText:cellData];

    return cell;

}

When we ask the collectionView to dequeue a cell, it will hand back an instance of UICollectionViewCell, because that’s all the dequeue method knows about. As we want to use our custom subclass, we first need to cast the UICollectionViewCell into an instance of CVCell. That then give us access to the titleLabel property, which can be set with the line:

[cell.titleLabel setText:cellData];

If you run the app again, it will look exactly the same as it did before – the difference is that we’ve created cells as instances of a custom UICollectionViewCell subclass.

Comments