UICollectionView Example

We’re going to take a look at the new UICollectionView class Apple introduced in iOS 6. It’s used to display a grid of cells, and what better to display in those cells than some beautiful photos from 500px?

I’ve got an example project completed and hosted on github that you can check out and play with. Don’t forget to register yourself with 500px first to get a consumer key and consumer secret, or else the API won’t work for you. Copy these into a text file because you’ll need them later.

Xcode might introduce a new template for collection view apps, but in the mean time, we’ll have to modify an existing project that uses UITableView.

Create a new Xcode project using the “Master-Detail Application” template.

Open the main Storyboard file. We’re going to throw away the existing table view and replace it with a great new collection view. The easiest way to do that is to select the existing Master View Controller, shown below, and hit the delete key.

D64FDC4D8F644D60A15675757BC8EA95

Now that the Master View Controller is missing, we need to re-add it. Go to your Master View Controller’s .h file and change it from inheriting from UITableViewController to UICollectionViewController. You’ll get some compiler errors; ignore them for now.

Drag a new UICollectionViewController form the library. Right click and drag from the Navigation Controller onto the collection view controller and select the “root view controller” outlet.

BDF26DDF70744C57813ED87D31033FAF

Right. Almost there. Open the identity inspector (⌘⎇3) and give the new UICollectionViewController the class name of your Master View Controller.

3B0B5635D36F497B84851AA27A9494C7

We’ll worry about the segues later. For now, go to the UICollectionViewController subclass’ .m file. We’ll fix those compiler errors now.

8A0115CB76E64B218DCD9E158D6AF93D

It’s always something with you, isn’t it, Xcode?

We’re going to gut the contents of the master view controller that deal with the tableView property (which we replaced). For this example, I’m deleting the following method:

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath

Everywhere else you see tableView cuasing a problem, replace it with collectionView. The two classes behave almost identically, so it’ll be an easy conversion.

The only problem I had was

[self.collectionView indexPathForSelectedRow];

UICollectionView doesn’t have the method indexPathForSelectedRow, so replace that line with the following:

[[self.collectionView indexPathsForSelectedItems] lastObject];

You’ll also need to modify the insertSections:withRowAnimation: method calls, since UICollectionView doesn’t have row animations; just remove the second part of the selector name.

Once you deal with all of the transition, you have a an app that runs, but is empty. Let’s fill it with beautiful photos.

226450BE3A464F44B4A4B8706BDC0612

We need a data model. Since we’re smart, accomplished, and handsome developers, let’s use Core Data. Create the following Core Data model.

3E628B26764C4ACDA973588CAE0C2678

All we need is a photoRating to sort by and photoImageData to display actual images, but let’s add a photoName for the detail view.

We need to configure the prototype cell used by the collection view. Create a new class, subclassing UICollectionViewCell, create a UIImageView outlet and appropriate instance methods to set the image.

Go to the Storyboard and select the protoype cell in the collection view. In the identity inspector, set it’s class to the UICollectionView subclass you just created. Add a UIImageView to the prototype cell and connect the outlet.

2E0D5ECB395F41DEA921597398D5E79E

Make sure to set the Identifier in the Storyboard or the collection view won’t dequeue the proper class.

We need to queue up the updates from our NSFetchedResultsController, since UICollectionView can only perform batch updates (with animation). Serious oversight on Apple’s here, in my opinion, since it makes using NSFetchedRsultsController with UICollectionView very sticky.

You basically need to cache the updates to the sections and objects individually until the fetched results controller is finished with the updates. This is because of the limitations imposed on UICollectionView.

Note: We’re using batch updates because we are inserting more than one cell at a time, and we want that nice animation when adding each individual cell. You could call reloadData once all the results are loaded, but you wouldn’t have any animations, which are part of the delight of NSCollectionVIew.

Creating the NSFetchedResultsController is almost the exact same as the project template:

- (NSFetchedResultsController *)fetchedResultsController
{
    if (_fetchedResultsController != nil) {
        return _fetchedResultsController;
    }

    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    // Edit the entity name as appropriate.
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"AFPhotoModel" inManagedObjectContext:self.managedObjectContext];
    [fetchRequest setEntity:entity];

    // Set the batch size to a suitable number.
    [fetchRequest setFetchBatchSize:20];

    // Edit the sort key as appropriate.
    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"photoName" ascending:NO];
    NSArray *sortDescriptors = @[sortDescriptor];

    [fetchRequest setSortDescriptors:sortDescriptors];

    // Edit the section name key path and cache name if appropriate.
    // nil for section name key path means "no sections".
    NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:@"Master"];
    aFetchedResultsController.delegate = self;
    self.fetchedResultsController = aFetchedResultsController;

    NSError *error = nil;
    if (![self.fetchedResultsController performFetch:&error]) {
         // Replace this implementation with code to handle the error appropriately.
         // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }

    return _fetchedResultsController;
}

Create two NSMutableArray instance variables and instantiate them in viewDidLoad. Implement the following NSFetchedResultsControllerDelegate methods.

- (void)controller:(NSFetchedResultsController *)controller 
    didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
    atIndex:(NSUInteger)sectionIndex 
    forChangeType:(NSFetchedResultsChangeType)type
{

    NSMutableDictionary *change = [NSMutableDictionary new];

    switch(type) {
        case NSFetchedResultsChangeInsert:
            change[@(type)] = @[@(sectionIndex)];
            break;
        case NSFetchedResultsChangeDelete:
            change[@(type)] = @[@(sectionIndex)];
            break;
    }

    [_sectionChanges addObject:change];
}

- (void)controller:(NSFetchedResultsController *)controller 
    didChangeObject:(id)anObject
    atIndexPath:(NSIndexPath *)indexPath 
    forChangeType:(NSFetchedResultsChangeType)type
    newIndexPath:(NSIndexPath *)newIndexPath
{

    NSMutableDictionary *change = [NSMutableDictionary new];
    switch(type)
    {
        case NSFetchedResultsChangeInsert:
            change[@(type)] = newIndexPath;
            break;
        case NSFetchedResultsChangeDelete:
            change[@(type)] = indexPath;
            break;
        case NSFetchedResultsChangeUpdate:
            change[@(type)] = indexPath;
            break;
        case NSFetchedResultsChangeMove:
            change[@(type)] = @[indexPath, newIndexPath];
            break;
    }
    [_objectChanges addObject:change];
}

These two methods queue the updates to the collection view. The following method waits for the fetched results controller to be finished before dequeuing those updates and applying them to our collection view.

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
    if ([_sectionChanges count] > 0)
    {
        [self.collectionView performBatchUpdates:^{

            for (NSDictionary *change in _sectionChanges)
            {
                [change enumerateKeysAndObjectsUsingBlock:^(NSNumber *key, id obj, BOOL *stop) {

                    NSFetchedResultsChangeType type = [key unsignedIntegerValue];
                    switch (type)
                    {
                        case NSFetchedResultsChangeInsert:
                            [self.collectionView insertSections:[NSIndexSet indexSetWithIndex:[obj unsignedIntegerValue]]];
                            break;
                        case NSFetchedResultsChangeDelete:
                            [self.collectionView deleteSections:[NSIndexSet indexSetWithIndex:[obj unsignedIntegerValue]]];
                            break;
                        case NSFetchedResultsChangeUpdate:
                            [self.collectionView reloadSections:[NSIndexSet indexSetWithIndex:[obj unsignedIntegerValue]]];
                            break;
                    }
                }];
            }
        } completion:nil];
    }

    if ([_objectChanges count] > 0 && [_sectionChanges count] == 0)
    {
        [self.collectionView performBatchUpdates:^{

            for (NSDictionary *change in _objectChanges)
            {
                [change enumerateKeysAndObjectsUsingBlock:^(NSNumber *key, id obj, BOOL *stop) {

                    NSFetchedResultsChangeType type = [key unsignedIntegerValue];
                    switch (type)
                    {
                        case NSFetchedResultsChangeInsert:
                            [self.collectionView insertItemsAtIndexPaths:@[obj]];
                            break;
                        case NSFetchedResultsChangeDelete:
                            [self.collectionView deleteItemsAtIndexPaths:@[obj]];
                            break;
                        case NSFetchedResultsChangeUpdate:
                            [self.collectionView reloadItemsAtIndexPaths:@[obj]];
                            break;
                        case NSFetchedResultsChangeMove:
                            [self.collectionView moveItemAtIndexPath:obj[0] toIndexPath:obj[1]];
                            break;
                    }
                }];
            }
        } completion:nil];
    }

    [_sectionChanges removeAllObjects];
    [_objectChanges removeAllObjects];
}

These methods should work for any UICollectionView and NSFetchedResultsController data.

Now that the NSFetchedResultsController is hooked up the the UICollectionView, let’s load some photos from 500px!

Use the consumer key and consumer secret from when you registered your application earlier. Follow the directions in the iOS 500px SDK to add the 500px SDK to your Xcode project. Make sure to follow the instructions in the SDKs README to set up your project to use the SDK.

[PXRequest setConsumerKey:@"your consumer key" consumerSecret:@"your consumer secret"];

[PXRequest requestForPhotoFeature:PXAPIHelperPhotoFeaturePopular completion:^(NSDictionary *results, NSError *error) {
    if (error)
    {
        [[[UIAlertView alloc] initWithTitle:@"Couldn't fetch from 500px." message:error.localizedDescription delegate:nil cancelButtonTitle:nil otherButtonTitles:@"OK", nil] show];
        return;
    }

    NSArray *photoArray = [results valueForKey:@"photos"];

    for (NSDictionary *photoDictionary in photoArray)
    {
        NSManagedObject *photoModel = [NSEntityDescription insertNewObjectForEntityForName:@"AFPhotoModel" inManagedObjectContext:AppDelegate.managedObjectContext];
        [photoModel setValue:[photoDictionary valueForKey:@"rating"] forKey:@"photoRating"];
        [photoModel setValue:[photoDictionary valueForKey:@"name"] forKey:@"photoName"];

        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            NSString *urlString = [[[photoDictionary valueForKey:@"images"] objectAtIndex:0] valueForKey:@"url"];
            NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:urlString]];
            dispatch_async(dispatch_get_main_queue(), ^{
                [photoModel setValue:imageData forKey:@"photoImageData"];
            });
        });
    }
}];

The fetched results controller will take care of that magic for us. We just need to configure our cells.

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    AFCollectionViewCell *cell = (AFCollectionViewCell *)[collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];

    NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
    [cell setImage:[UIImage imageWithData:[object valueForKey:@"photoImageData"]]];

    return cell;
}

E8ECC8892EEB418D9FE04C6A04AD5436

I went to the liberty of adjusting the sizes to make things pretty - nice app! Let’s connect the detail view controller now.

Open the Storyboard and right-click on the prototype cell. We’ll be connecting the selection triggered segue to the detail view controller. Drag the circle beside selection to the detail view controller and choose “push” from the menu that appears.

AF66949DE9D6407C8288A2B406292DB8

Select the segue and make sure its identifier is set to “showDetail”.

Replace the UILabel with a UIImageView, change the IBOutlet in the header file, reconnect the outlet, and modify configureView to look like the following:

- (void)configureView
{
    // Update the user interface for the detail item.

    if (self.detailItem)
    {
        self.detailImageView.image = [UIImage imageWithData:[self.detailItem valueForKey:@"photoImageData"]];
        self.title = [self.detailItem valueForKey:@"photoName"];
    }
}

In the master view controller, prepareForSegue:sender: should look like the following:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([[segue identifier] isEqualToString:@"showDetail"]) {
        NSIndexPath *indexPath = [[self.collectionView indexPathsForSelectedItems] lastObject];
        NSManagedObject *object = [[self fetchedResultsController] objectAtIndexPath:indexPath];
        [[segue destinationViewController] setDetailItem:object];
    }
}

Run the app, tap on a photo, and you’ll see the following:

2E826D143C214E178029F7B19AE16936

Looks great!

If you have any questions, please let me know. Check out the code from GitHub and have fun!


Please submit typo corrections on GitHub