The Collection View: Part 2 of 6

Last week you were introduced to the Collection View Control and its components. You got a bird’s eye view of the BevUp! application we will create. I also gave you a bird’s eye view of the application’s data model.

Our Objectives

Today you’ll create the BevUp! application. You will design the first view to look like Figure 1 below. You will add code in the view’s class files to implement “System Tasks” listed in the table below.

App User Tasks System Tasks
The user launch the application.
drinkrecipes-screen
The system respond by performing these tasks.

  1. Load an image in the Collection View’s background.
  2. Register the RecipeCell class with the Collection View.
  3. Fetch all array items from the plist file and load them in a mutable array.
  4. Download drink recipes images and load them in the Collection View Cells.
  5.  Display recipe names below each Collection View Cell.
  6. Display the Drink Recipes View in the iPad Simulator.

Create The BevUp! Application

Launch Xcode and create a new project using the “Single View Application” template. Provide options shown in Figure 3 below.

bevUp-fig01
Figure 3: New Xcode project options

Take a look at Figure 4 below, it shows what the template created for us. As you can see, it created a default View Controller scene on the storyboard’s canvas and a viewController class in the Project Navigator for us. The template also connected the scene to the ViewController class for us.

bevUp-fig02
Figure 4: The BevUp! project’s folders and files

Setting Up Shop

We’ll have to perform several tasks before perform systems tasks which involves designing Drink Recipe view and coding its class files.

Step 1: Change The Class Name

Click the ViewController.h file, then highlight the class name on line 11-see image below. Use Xcode’s Edit | Refactor | Rename menu item to change the ViewController class name to DrinkRecipesViewController.h

bevup-refactor

Step 2: Add Images in The Project

We’ll need an image to use as the Collection View’s background. We’ll also need an image as a placeholders in the the Collection View’s image view controls. Download this file; contain images we’ll use for the Collection View’s background, and as a placeholder for the Collection View Cells’ image views.
bevup-assets
After unzipping the file, drag-and-drop all four images in the Images.xcasset folder.

Step 3: Add The Plist File in The Project

I’ve already created the plist file we’ll use in the BevUp! application. All you have to do is download and unzip it, then drag-and-drop the DrinkList.plist file in the BevUp! folder.
DrinkList

As you can see in Figure 5 below, the plist file has an array called Root and it has 33 array items. One of the Root array item is expand in the image and I’ve noted its elements. In fact, you can think of each “array element” of the Root array as a Recipe object with five properties as shown in the class diagram in Figure 6.

drinkrecipes-plistfile bevup-recipe-class
Figure 5: The DrinkList.plist file Figure 6: Class Diagram of a Recipe object
Step 4: Download The SDWebImage Library

The app will have to download images from web address that point to recipe image, then load them in 33 Collection View Cells’ Image View controls. The download process will interfere with the Collection View’s scrolling mechanism-scrolling will be jerky. To fix that problem, we’ll have the app download the drink recipe images in the background, while the user is using the the app. While images are downloading, the app will load an image called placeholder.png in the Collection View Cells’ Image View controls.

Here is the link to download the library that’ll enable the app to download web images in the background.
SDWebImage-master

Once the download is complete, unzip the file. Expanding the SDWebImage-master folder, drag-and-drop the SDWebImage folder in the application’s BevUp! folder.

Step 5: Import The UIImageView+WebCache.h File

Add this import directive in the DrinkRecipesViewController.h file.

By importing the UIImaeView+WebCache.h file in our class, we’ll be able to use the setImageWithURL: method in the DrinkRecipesViewController.m file. That method will download drink recipe images in the background while the app is running and load the placeholder.png image in the Collection View Cells’ Image View controls.

Step 6: Configure the iOS Simulator

Launch the iOS Simulator as shown in the image below. Select Window > Scale > 75%. Then select Hardware > Device > iPad. Switch to Xcode and set the active schema on the Toolbar to iPad. That’s it we’re done setting up shop.
launch-the-simulator

Design The Drink Recipes Scene

It is time to design the BevUp! application’s Drink Recipes View Controller scene. Start by click the Main.storyboard file to load it in the storyboard file in Interface Builder.

  1. Add a Search Bar on the Drink Recipes View Controller Scene.
  2. Use its sizing handle to match the view’s own.
  3. In the Attributes inspector, type “Enter the recipe name here” in the Placeholder box.
  4. Add a Collection View on the scene, just below the Search Bar.
  5. Use its sizing handles to cover the view’s surface.
  6. Click the Collection View, then in the Size inspector I want you to set these attributes for the Collection View Size item.
    collectionview-sizeinspector
  7. Click the Attributes inspector and set the Scroll Direction to Horizontal.

Here’s what the Drink Recipes View Controller Scene should look like now.

bevUp-fig04a

Test The DrinkRecipes View’s Interface

Now is a good time as any to run the application to test the DrinkRecipes view’s interface in the iPad simulator. So run the application and the Drink Recipes view will look like this:
bevUp-fig06a

The RecipeCell Class

Ok, we have to add code in the DrinkRecipesViewController files to populate the view’s Collection View with Collection View Cells; but first, we have to create a class called RecipeCell in the project. It will contain properties and methods shown in the class diagram you see in Figure A below.

class_recipecell collectionview-cellsketch2
Figure A Figure B

The Class Properties

deleteButtonImg – This property will hold an image we’ll create programmatically and we’ll add it on the deleteButton. Figure B above shows what it will look like in the upper left corner of a Collection View Cell.

MARGIN – This constant defines the red circle’s dimension. We’ll use it in code that create the image for the deleteButton.

The Class Methods

initWithFrame: – This a Cocoa initializer method. It takes a single parameter and return object of type id or nil. Further more, the method is automatically fired when we use the RecipeCell class to create an object.

The initWitFrame: method’s job is to create a custom Collection View Cell and return it to its receiver, which is the Collection View we added on the Drink Recipes View Controller scene. The reason why the method create and return a Collection View Cell is because we made the RecipeCell class a “Subclass of” of the UICollectionViewCell class. Don’t forget that the UICollectionViewCell class is for creating Collection View Cells, for a Collection View.

Now, we will add three statements in the method. The first statement we’ll add in the initWithFrame: method will invoke its superclass (supper) initializer, assign the result that class’ initializer returns, to the RecipeCell class’ self object; which is a frame object. If the superclass returns a nil object (nothing), then the initWithFrame: method is unable to do its job; so it returns the nil object to its receiver; as you already know is a Collection View.

The next statement we’ll add in the initWithFrame: method is an If() statement; its job is to check the self object to see if contain a frame object for a Collection View Cell. If the self object have a frame, code within the if() block are executed. Now, we’ll have to add code in the if() block to do the following:

  • Create and initialize UIView object called insetView.
  • Initialize the imageView object we declared in the RecipeCell.h file.
  • Customize the imageView object by setting a few of its properties. One of these settings will make the imageView’s corners round.
  • Add the customized imageView in the insetView.
  • Initialize the label object we declared in the RecipeCell.h file.
  • Customize the label by setting a few of its properties. Fore example; we’ll set the label’s numberOfLines property to, 2. We’ll also set the label’s lineBreakMode property to add words on the second line, if they don’t fit on the first line.
  • Add the customized label in the insetView.
  • Initialize the deleteButton object we declared in the RecipeCell.h file.
  • Create a frame called buttonFrame and initialize it with the deleteButton’s frame.
  • Create and initialize a red circle with a white cross hair in it; then put it in the deleteButtonImg object.
  • Set the deleteButton’s image property with the deleteButtonImg we just created.
  • Add the deleteButton object in the Collection View’s contentView object.

The final statement in the initWithFrame: method we’ll will return to its receiver (a Collection View) a Collection View Cell or nil (nothing).

Take a look at this box; it shows the hierarchy of objects the initWithFrame: method creates in the Collection View’s contentView object. By the way, the contentView is a subview of a Collection View Cell.

So to recap, the initWithFrame: method takes a single parameter called, frame. It creates a Collection View Cell with two custom objects, which are called subviews. Return the Collection View Cell to its receiver (a Collection View). The method also create a custom deleteButton object and a custom UIView object called insetView, then add both objects in the Collection View’s contentView.

startShakingIcons  – First of all this method’s job is to animate (shake) the Collection View’s Cells. We’ll use an instance of the CABasicAnimation class to animate the Collection View Cells on request. The method takes no parameter and return nothing. Also, the CABasicAnimation class is located in the QuartzCore library; therefore, we’ll have to import it in the RecipeCell.h file.

stopShakingIcons – This method does the opposite of the startShakiingIcons method-it stop animating (shaking) the Collection View Cells. It will also set the deleteButton’s hidden boolean variable to YES; thus hiding the deleteButton object that’ll appear in the upper-left corner of a Collection View Cell as shown in Figure B above.

Create The RecipeCell Class in The Project

Ok, so you’ve gotten an intimate view of the RecipeCell class. Now, use these images as a guide to create it in the project. Make it a Subclass of the UICollectionViewCell class.
add-newfile-inthe-project
Once Xcode have created the class in the project, return to the storyboard file to connect the RecipeCell class to the Collection View Cell. Click the Collection View Cell in the Document Outline. In the Identity inspector, select “RecipeCell” from the Class drop-down menu.

collectionview-set-cellclass

Add Code in The RecipeCell Files

Ok, here is the code to add in the RecipeCell.h file.

#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>

@interface RecipeCell : UICollectionViewCell {
  UIImage *deleteButtonImg;
  #define MARGIN 2
}

@property (nonatomic, strong) UILabel *label;
@property (nonatomic, strong) UIImageView *imageView;
@property (nonatomic, strong) UIButton *deleteButton;

- (void)startShakingIcons;
- (void)stopShakingIcons;

@end

We imported the QuartzCore class so we can animate the Collection View Cells. Next, we declared the class properties and methods.

  • The deletButtonImg is for holding the round red button image we’ll create in the implementation file’s initWithFrame: method.
  • The MARGIN constant determines the deleteButtonImg dimension. The label control is for holding a drink recipe’s name.
  • The imageView is for holding a drink recipe image, which we’ll customize to look like an icon with round corners.
  • The deleteButton will display an image which we’ll create in the implementation file’s initWithFrame: method.
  • The startShakingIcons method will animate (shake) the Collection View Cells and labels.
  • The stopShakingIcons will stop the animation.

Moving along, here is the code to add in the RecipeCell.m file.

#import "RecipeCell.h"

static UIImage *deleteButtonImg;
#define MARGIN 2

@implementation RecipeCell

// This method create a new Collection View Cell, code in the if statement add an imageView,
// a Label, and a deleteButton in it
- (id)initWithFrame:(CGRect)frame
{
  self = [super initWithFrame:frame];
  if (self) {
    // Create a container for holding other objects (imageView and label)
    UIView *insetView = [[UIView alloc] initWithFrame:CGRectInset(self.bounds,   self.bounds.size.width/8, self.bounds.size.height/8)];

    // Add the container in the Collection View Cell's contentView container
    [self.contentView addSubview:insetView];

    // Initialize the imageView, customize its appearance, then add it in the insertView
    self.imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, insetView.frame.size.width, insetView.frame.size.height)];
    self.imageView.backgroundColor = [UIColor clearColor];
    float dim = MIN(self.imageView.bounds.size.width, self.bounds.size.height);
    self.imageView.clipsToBounds = YES;
    self.imageView.layer.cornerRadius = dim/9;
    self.imageView.layer.opacity = 1.0;
    self.imageView.layer.borderColor = [UIColor lightGrayColor].CGColor;
    self.imageView.layer.borderWidth = 1.0;
    self.imageView.userInteractionEnabled = YES;
    [insetView addSubview:self.imageView];

   // Initialize the label, customize its appearance, then add it in the insertView
   self.label = [[UILabel alloc] initWithFrame:CGRectMake(0, 100, insetView.frame.size.width, insetView.frame.size.height - 59)];
   self.label.textAlignment = NSTextAlignmentCenter;
   self.label.numberOfLines = 2;
   self.label.lineBreakMode = NSLineBreakByWordWrapping;
   self.label.clipsToBounds = YES;
   self.label.textColor = [UIColor darkGrayColor];
   self.label.font = [UIFont boldSystemFontOfSize:15];
   self.label.layer.opacity = 1.0;
   [insetView addSubview:self.label];

   // Initialize the deleteButton
   self.deleteButton = [[UIButton alloc] initWithFrame:CGRectMake(frame.size.width/16, frame.size.width/16, frame.size.width/4, frame.size.width/4)];

   CGRect buttonFrame = self.deleteButton.frame;

   // Create a red circle image with a white crosshair
   UIGraphicsBeginImageContext(buttonFrame.size);
   CGFloat sz = MIN(buttonFrame.size.width, buttonFrame.size.height);
   UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(buttonFrame.size.width/2, buttonFrame.size.height/2) radius:sz/2-MARGIN startAngle:0 endAngle:M_PI * 2 clockwise:YES];
   [path moveToPoint:CGPointMake(MARGIN, MARGIN)];
   [path addLineToPoint:CGPointMake(sz-MARGIN, sz-MARGIN)];
   [path moveToPoint:CGPointMake(MARGIN, sz-MARGIN)];
   [path addLineToPoint:CGPointMake(sz-MARGIN, MARGIN)];
   [[UIColor redColor] setFill];
   [[UIColor whiteColor] setStroke];
   [path setLineWidth:3.0];
   [path fill];
   [path stroke];
   deleteButtonImg = UIGraphicsGetImageFromCurrentImageContext();
   UIGraphicsEndImageContext();

   // Add the red circle image in the deleteButtonImg object
   [self.deleteButton setImage:deleteButtonImg forState:UIControlStateNormal];
   [self.contentView addSubview:self.deleteButton];
 }
  // Return the newly created Collection View Cell to its reciever, which is a Collection View
  return self;
}

- (void)startShakingIcons
{
  CABasicAnimation *shakeAnim = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
  float startAngle = (-2) * M_PI/180.0;
  float stopAngle = -startAngle;
  shakeAnim.fromValue = [NSNumber numberWithFloat:startAngle];
  shakeAnim.toValue = [NSNumber numberWithFloat:3 * stopAngle];
  shakeAnim.autoreverses = YES;
  shakeAnim.duration = 0.2;
  shakeAnim.repeatCount = HUGE_VALF;
  float timeOffset = (float)(arc4random() % 100)/100 - 0.50;
  shakeAnim.timeOffset = timeOffset;

   // Create a layer, then add animation to it
   CALayer *layer = self.layer;
   [layer addAnimation:shakeAnim forKey:@"shaking"];
}

- (void)stopShakingIcons
{
  CALayer *layer = self.layer;
  [layer removeAnimationForKey:@"shaking"];
  // Hide all delete buttons
  self.deleteButton.hidden = YES;
}
@end

Add Code in The DrinkRecipesViewController Files

What we have to do now is add these statements in the DrinkRecipesViewController.h file.

#import <UIKit/UIKit.h>
#import "RecipeDetailViewController.h"
#import "RecipeCell.h"
#import "SDWebImage/UIImageView+WebCache.h"

@interface DrinkRecipesViewController : UIViewController

@property(nonatomic, weak) IBOutlet UISearchBar *searchBar;
@property(nonatomic, weak) IBOutlet UICollectionView *collectionView;
@property(nonatomic, strong) NSMutableArray *searchResults;
@property (strong, nonatomic) NSMutableArray *drinkList;
@property (strong, nonatomic) NSArray *recipeSelectedArray;
@property (readwrite, nonatomic) BOOL isSearchOn;
@property (readwrite, nonatomic) BOOL shouldAnimate;
@property (strong, nonatomic) RecipeCell *icon;

@end

Ok, you’re ready to add code in the DrinkRecipesViewController.m file to implement system tasks listed above. So click the DrinkRecipesViewController.m file and enter this code in the ViewDidLoad method.

- (void)viewDidLoad
{
  [super viewDidLoad];

  // Register our RecipeCell class with the Collection View so we can use its properties and methods
  [self.collectionView registerClass:[RecipeCell class] forCellWithReuseIdentifier:@"Cell"];

  // Open the plist file and add its content in the drinkList array
  NSString *plistFilePath = [[NSBundle mainBundle] pathForResource:@"DrinkList" ofType:@"plist"];
  self.drinkList = [NSMutableArray arrayWithContentsOfFile:plistFilePath];

  // Set the Collection View to display a background image
  self.collectionView.backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"leather.png"]];
}

Now, run the application and the DrinkRecipes view will now look like this:

bevUp-fig06b

Connect The Collection View’s Data Source and Delegate Outlet

What you have to do now is return to the storyboard file to connect the Collection View’s data source and delegate outlet to the Drink Recipes View Controller.

  1. Click the Document Outline icon to show it.
  2. Expand the Drink Recipes View Controller tab.
  3. Control-drag from the Collection View to the Drink Recipes View Controller. Release your mouse then select dataSource from the pop-up menu.
    bevUp-fig07
  4. Repeat step 3 to connect the Collection View’s delegate property to the Drink Recipes View Controller.

By connecting the Collection View’s dataSource property to the Drink Recipes View Controller, methods of the  UICollectionViewDataSource protocol we’ll implement in the DrinkRecipesViewController.m file will work. The same is true for the UICollectionViewDelegate protocol’s methods.

Implement The Collection View’s Data Source Methods

What you have to do now is implement these data source methods of the UICollectionViewDataSource protocol in the DrinkRecipesViewController.m file. They are responsible for displaying Collection View Cells in the Collection View.

- (NSInteger)collectionView:(UICollectionView *)view numberOfItemsInSection:(NSInteger)section
{
  if (self.isSearchOn)
    return self.searchResults.count;
  else
    return self.drinkList.count;
}

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

- (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
  // Initialize the reusable Collection View Cell called icon
  _icon = [cv dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
  NSArray *recipeArray;
  if (self.isSearchOn) {
    // Search IS on
    recipeArray = [[NSArray alloc] init];
    recipeArray = [self.searchResults objectAtIndex:indexPath.row];
  } else {
    // Search ISN'T on
    recipeArray = [[NSArray alloc] init];
    recipeArray = [self.drinkList objectAtIndex:indexPath.row];
  }
  // These statements feth a recipe image from the remote server and place it in the Collection View Cell's imageView object
  NSString *imageUrl = recipeArray[1];
  NSURL *url = [[NSURL alloc] initWithString:imageUrl];
  [_icon.imageView setImageWithURL:url placeholderImage:[UIImage imageNamed:@"placeholder.png"]];

  // Add a recipe name in the label object
  _icon.label.text = recipeArray[0];

  // Configure the deleteButton's action method
  [_icon.deleteButton addTarget:self action:@selector(delete:) forControlEvents:UIControlEventTouchUpInside];

  // Hide or show the deleteButton in the Collection View's contentView object
  if (self.shouldAnimate == NO) {
    _icon.deleteButton.hidden = YES;
  } else {
    _icon.deleteButton.hidden = NO;
  }
  // Return the Collection View Cell
  return _icon;
}

As you can see Collection View’s data source methods are very much like the Table View’s data source methods. Since we connected the Collection View’s data source property to the DrinkRecipesViewController Interface Builder, the methods will work.

numberOfSectionsInCollectionView:

This is optional data source method and it returns the answer to this question: How many sections does the collection view contain? If we didn’t implement this method in the DrinkRecipesViewController.m file, then the collection view uses a default value of 1.

numberOfItemsInSection:

This is a required data source method and it returns the answer to this question: How many items there are in a section of the Collection View? The if statement we added in the method is responsible for figuring out and returning the answer to that question. It check to see if the user is searching for a drink recipe; if so, then count and return the total number of elements there are in the searchResults array. If the user is not searching for a drink recipe then count and return the total number of elements there are in the drinkList array. The else statement will return the integer 33. That’s because there are 33 array items in the plist file.

cellForItemAtIndexPath:

This is a required data source method and it return the answer to this question: For a given section or item, what views should be used to display the corresponding content? Code we entered in the method basically check to see if the user is searching for a drink recipe by name, if so then display content of the searchResults array, in the Collection View Cells. If the user is not searching for a drink recipe, then display content of the drinkList array, in the Collection View Cells. The final piece of code we entered in the method determines whether delete buttons are shown or not shown in the upper left corner of the Collection View.

Run The Application

When you run the application now, you’ll see output shown in Figure A below. Awsome! We have data and we can scroll the view. Notice how the iPad screen look very much like our sketch of the Drink Recipes View Controller screen.

collection-view-output1 drinkrecipes-screen
Figure A: The Drink Recipes View Controller screen in the iPad Simulator Figure B: Sketch of the Drink Recipes View Controller screen

As you can see, nothing happens when you click (tap) or long-click (long-press) a recipe icon. We’ll have to add more code in the DrinkRecipesViewController.m file to fix that. The Search Bar doesn’t work either. We’ll have to add code in the file to fix that as well.

That’s it! We’ve implemented all of the systems tasks, which are these:

  1. Load an image in the Collection View’s background.
  2. Register the RecipeCell class with the Collection View.
  3. Fetch all array items from the plist file and load them in a mutable array.
  4. Download drink recipes images and load them in the Collection View Cells.
  5. Display recipe names below each Collection View Cell.
  6. Display the Drink Recipes View in the iPad Simulator.

We’ll continue developing the BevUp application next week. Until then, happy coding ! 🙂