Building a Grid Layout with UICollectionView and Realm

Cover image of the Realm Grid Controller in action

Overview

Looking to build a grid layout in your app?

With your data stored in Realm, you can create an interface like this really quickly. Best of all, changes to your data will trigger automatic animations!

This tutorial is going to show you how you can use ABFRealmGridController, a specialized subclass of UICollectionViewController, to bind to your data in Realm.

Let’s get started, but if you want to jump to the end product, you can see the final code on GitHub here.

If you’re looking for a Swift version, you can check out the tutorial for RealmGridController, which has an identical API.

This tutorial requires Xcode 7+.

Tutorial

Create a new Xcode project, using the “Single View Application” template. Be sure “Language” is set to Objective-C, and that “Use Core Data” is unchecked.

ABFRealmGridController is available through CocoaPods. (If you don’t have CocoaPods installed, follow their instructions before continuing.) In your terminal, cd to the project folder you just created, and open the Podfile (if you don’t have a podfile, perform pod init in the folder first). To install ABFRealmGridController, simply add the following lines to the Podfile you just created:

pod 'ABFRealmGridController', '1.4.1'

For this tutorial, we will be using a few more pods, so add the following lines too:

# Pre-built data model for New York Times Top Stories API
pod 'RealmNYTStories'

# Image loading/caching library
pod 'Haneke'

# Easy UIViewController to display website
pod 'TOWebViewController'

In your terminal, run pod install. This will also install Realm Swift automatically! (If this is your first time using CocoaPods, this could take a while. Future uses will be much faster.)

When it’s done, you’ll need to close the Xcode window and open the xcworkspace file that CocoaPods created, so that you can use the classes inside the pods.

Now that we have our base project setup, we need to first create a subclass of ABFRealmGridController to allow us to customize its display.

Go to File → New → File then click “Cocoa Touch Class.” In the resulting dialog, set the name to “MainGridController” and the subclass to UICollectionViewController (we will update the subclass to the grid controller later). Be sure to leave the checkbox for “Also create XIB file” unchecked and the Language as Objective-C.

Create MainGridController

With our subclass created, we now have to customize the Storyboard to utilize it. In Main.storyboard, delete the default ViewController scene. Then drag into the scene a UINavigationController and a UICollectionViewController. You will have to delete the UITableViewController that the UINavigationController created and change the segue to point to the UICollectionViewController (control-click the Navigation Controller and drag to the UICollectionViewController then select “Relationship Segue”: “root view controller”). For a demonstration of how to do this, check out the first half of the video just below this.

With the UINavigationController and UICollectionViewController in place, make sure to set the UICollectionViewController to have a custom class of MainGridController, and set the background color on the UICollectionView to white.

Then make sure the UINavigationController is listed as the initial view controller, or you won’t ever see anything! Here’s a video showing all three of the steps above:

Finally, we need to configure the navigation bar within our grid controller. Click the Navigation Item (you can click the navigation bar in the grid controller) and set the title to “Top Stories.” Then drag a UIBarButtonItem to the right side of the navigation bar. Configure the bar button’s “System Icon” to be “Refresh,” then click the Assistant Editor button (it looks like two overlapping circles). Control-click the refresh button and drag into the code file to create an action outlet in MainGridController; name the method didClickRefreshButton and set the “Type” to UIBarButtonItem.

Tip: You can now delete the ViewController.h and ViewController.m files as we won’t need them anymore.

Now that we have the initial wiring set up, we need to get the data ready. The New York Times offers a great API, and we will use their “Top Stories” API to retrieve the latest news stories and display them in the grid.

Recall that during the pod install we added RealmNYTStories. This pod includes two Realm model classes: NYTStory and NYTStoryImage. They represent the data returned from the New York Times Top Stories API.

To simplify this tutorial, we won’t go into all of the steps on how to request data from the API and then deserialize the resulting JSON data into Realm objects. Instead, all you have to do is register for an API Key on the New York Times site: http://developer.nytimes.com/apps/register. Be sure to scroll all the way down and check the box for “Issue a new key for Top Stories API”!

Then simply add the following lines to the top of MainGridController.m, which will include everything we need:

#import <RealmNYTStories/NYTStory.h>
#import <RealmNYTStories/NYTStoryImage.h>
#import <Realm/Realm.h>

Now, in MainGridController.h, adjust MainGridController to be a subclass of ABFRealmGridController, instead of UICollectionViewController:

#import <ABFRealmGridController/ABFRealmGridController.h>

@interface MainGridController : ABFRealmGridController

@end

To load data from the New York Times, the NYTStory class includes a convenience method, so insert this method in the didClickRefreshButton: method that is tied to the refresh bar button. Don’t forget to include your own API key!

- (IBAction)didClickRefreshButton:(UIBarButtonItem *)sender
{
    [NYTStory loadLatestStoriesIntoRealm:[RLMRealm defaultRealm]
                              withAPIKey:@"INSERT_YOUR_API_KEY_HERE"];
}

If you want to see how we handle the data request and JSON parsing, just check out the implementation of this method in NYTStory.m. It performs several async API requests for each newspaper section and parses the JSON response into the Realm models.

Now that we have our data model ready, let’s get started on customizing the grid’s look. Since ABFRealmGridController is just a specialized UICollectionView, we will need to create a UICollectionViewCell to display each news story.

Go to File → New → File then click “Cocoa Touch Class.” In the resulting dialog, set the name to “MainCollectionViewCell” and the subclass to UICollectionViewCell. Be sure to leave the checkbox for “Also create XIB file” unchecked and the Language as Objective-C.

Create UICollectionViewCell

For each news story, we want to show an image, the publishing date, the title, and a small excerpt. Add the following properties to the MainCollectionViewCell:

@interface MainCollectionViewCell : UICollectionViewCell

@property (strong, nonatomic) IBOutlet UIImageView *imageView;

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

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

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

@end

To layout the elements, we will use Main.storyboard. Customize the UICollectionView by making sure “Items” is set to 1. Adjust the cell size to be 185 wide and 250 high. All the other sizes should be 0.

Now drag in a UIImageView and position it at the top.

Apply the following constraints. There is a video below, showing how to setup the UIImageView:

Constraint Value
Horizontally in container 0
Top Space 0
Width 75
Height 75

Next, we’ll set up the three labels. There’s a video after this showing how to set them up.

First, drag a UILabel and position it below the UIImageView. This will be the title label. Set its font to System Medium 15, and center the text.

Apply the following constraints:

Constraint Value
Top Space 4
Leading Space 0
Trailing Space 0
Height 21

Second, drag another UILabel below the title label. This will be the date label. Set its font to System 13 with a hex color of #8C8C8C, and center the text. (If you don’t see the hex color field, be sure you’re on the second tab in the color picker, and that “RGB Sliders” is selected.)

Apply the following constraints:

Constraint Value
Top Space 4
Leading Space 0
Trailing Space 0
Height 21

Finally, drag the last UILabel for the excerpt below the date label. Set its font to System 13 with a hex color of #8C8C8C. Set the “Number of Lines” to 0, which will let the label use as many lines as it needs.

Apply the following constraints:

Constraint Value
Top Space 8
Leading Space 0
Trailing Space 0
Bottom Space (Priority Low) 0

Since the label could be any size, based on the amount of text, we need to make sure the bottom constraint has a lower priority to allow it to adjust.

Here is a video showing all of the steps involved in settings up the cell’s labels, including the specific constraints:

Now that the cell is configured, set its custom class to MainCollectionViewCell and give it the identifier “mainCell”. (It’s important that you type this in exactly as it’s written here, or the app won’t work later.)

You will now need to connect the elements in the cell to the corresponding outlets in MainCollectionViewCell.h. Open the Assistant Editor again, and control-click and drag from an element to the correct outlet. (If you don’t see MainCollectionViewCell.h in the Assistant Editor, you’ll have to manually set it by cliecking “Automatic” at the top of the pane, changing it to “Manual,” and navigating to MainCollectionViewCell.h.) You can see how in this video:

Almost done! We have our data model set up, and a custom UICollectionViewCell to display the stories. The last part is to configure MainGridController.

In MainGridController.m, set the reuseIdentifier to “mainCell”.

Then, in viewDidLoad, we need to tell the controller that we want to bind to the data model NYTStory.

Remove the line of code that is attempting to register a cell class; we have already set that up in the storyboard.

static NSString * const reuseIdentifier = @"mainCell";

- (void)viewDidLoad {
    [super viewDidLoad];

    // REMOVE THIS LINE
    // [self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:reuseIdentifier];

    self.entityName = @"NYTStory";
}

TIP: You can also set this in Main.storyboard. Select the MainGridController, and the property will be in the right sidebar. This is possible due to the @IBInspectable annotations.

We want to sort the stories based on the published date, so set the sortDescriptors property:

static NSString * const reuseIdentifier = @"mainCell";

- (void)viewDidLoad {
    [super viewDidLoad];

    self.entityName = @"NYTStory";

    self.sortDescriptors = @[[RLMSortDescriptor sortDescriptorWithProperty:@"publishedDate" ascending:NO]];
}

Since ABFRealmGridController is simply a specialized UICollectionViewController, we will need to implement a UICollectionViewDataSource method to configure the cells. Remove all the methods below the line #pragma mark <UICollectionViewDataSource> except for:

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

    // Configure the cell

    return cell;
}

First, add the necessary import statements at the top of the file with the others:

#import "MainGridController.h"
#import "MainCollectionViewCell.h"

#import <RealmNYTStories/NYTStory.h>
#import <RealmNYTStories/NYTStoryImage.h>
#import <Realm/Realm.h>
#import <Haneke/Haneke.h>

Next, configure the cell in collectionView:cellForItemAtIndexPath::

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

    MainCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier
                                                                             forIndexPath:indexPath];

    // Configure the cell
    NYTStory *story = [self objectAtIndexPath:indexPath];

    cell.titleLabel.text = story.title;
    cell.dateLabel.text = [NYTStory stringFromDate:story.publishedDate];
    cell.excerptLabel.text = story.abstract;

    // Use Haneke image caching
    [cell.imageView hnk_setImageFromURL:story.storyImage.url];

    return cell;
}

This method is where the UICollectionView requests the cell to display. We dequeue a cell with the identifier “mainCell” (recall that we set this in the storyboard).

We can then use the controller’s method to retrieve the object at the given index path. With the story, we can now set the various properties of the cell.

Note: NYTStory includes a class method to convert the date into a short string.

Finally, to handle displaying the image, we are using a simple image caching library called Haneke. Recall that we installed this during the initial pod install.

This library makes it really easy to fetch images from the web on a background thread and then cache them for quick access while scrolling.

First, we had to import the main header file:

#import <Haneke/Haneke.h>

Then, this line in collectionView:cellForItemAtIndexPath: enabled the caching:

[cell.imageView hnk_setImageFromURL:story.storyImage.url];

With the cell configured to display the data, there’s only one small step left before the app will work. Starting with iOS 9, we need to make an exception to the App Transport Security (ATS) settings so that the stories can load (by default, iOS 9 blocks all URL requests to non-secure websites).

Go to Info.plist and add the following entries (you can see the new entries on the last two lines):

Create App Security Transport Settings

Now that everything is configured, you can run the app and see the stories populate the grid! 👏

Now let’s make the app a bit more interesting…

First, let’s allow the user to click on the story and load it in a web view. Recall that we installed TOWebViewController during the initial pod install, so let’s import its header into MainGridController.m:

#import <TOWebViewController/TOWebViewController.h>

This library makes it dead-simple to display a web view with a URL. We will need to implement a UICollectionViewDelegate method:

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    [collectionView deselectItemAtIndexPath:indexPath animated:YES];

    NYTStory *story = [self objectAtIndexPath:indexPath];

    TOWebViewController *webController = [[TOWebViewController alloc] initWithURLString:story.urlString];

    UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:webController];

    [self.navigationController presentViewController:navController animated:YES completion:nil];
}

This method informs us when the user clicks a cell, so that we can grab the story based on the index path. With the story, we create an instance of TOWebViewController, passing in the story’s URL string. We then wrap the web view controller in its own UINavigationController and present it.

The final customization is to configure the collection view to have different number of columns based on the orientation. This allows us to really showcase the ease of use that UICollectionView offers for grid layouts.

To configure the layout, we need to implement a method from UICollectionViewFlowLayoutDelegate:

- (CGSize)collectionView:(UICollectionView *)collectionView
                  layout:(UICollectionViewLayout *)collectionViewLayout
  sizeForItemAtIndexPath:(NSIndexPath *)indexPath {

    CGFloat height = 250.0;

    if ([[UIApplication sharedApplication] statusBarOrientation] == UIInterfaceOrientationPortrait) {
        CGFloat columns = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad ? 3.0 : 2.0;

        CGFloat width = CGRectGetWidth(self.view.frame) / columns;

        return CGSizeMake(width, height);
    }
    else { // landscape
        CGFloat columns = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad ? 4.0 : 3.0;

        CGFloat width = CGRectGetWidth(self.view.frame) / columns;

        return CGSizeMake(width, height);
    }
}

This checks if the orientation is portrait or landscape, and if the app is running on an iPhone or an iPad. Based on the configuration, it adjusts the cell width so that they get draw in a given number of columns. For example, on an iPhone in landscape, the column count would be 3.

Now your app is ready to go. Simply run the app and click the refresh button to see your stories appear!



Realm Cocoa Team

Realm Cocoa Team