Saturday, November 4, 2017

How To: Use Reusable Views Correctly (TableView, CollectionView, etc)

First, some short intro! :D My blog views has finally reached ONE MILLION!!!

Now if I get a dollar for a view. Phew! None the less, I am so happy with this view count- why? Because it means I helped quite many developers to create their apps (hopefully). And those apps will benefit mankind. Even if it's a fart app, hey, it cheers up people and that is good.

As noobs we tend to not getting the whole picture of a particular UI object. A label is simple, there is text property, you set it directly in Storyboard or programatically (label.text = @"hello";) and it shows immediately. Pretty straight forward. But for objects that uses Reusable Cells like TableView, there are multiple set ups that we need to do to use it properly and therefore prevent any weird and unwanted bugs and crashes. Anyway here's what we're gonna end up with at the end of the tutorial:


It's a 2.4MB GIF.


What does Reusable Views mean? 

It is basically a concept/method to display big amount of data in a scrollable area. In case of TableView,  the amount of cells being created by TableView is only a small quantity that is enough to fit the visible area of TableView.

For example, if you have TableView of height 300, and each cells height is 10, then at least 31 cells are created at runtime. And when you scroll down the table, the cell that is being pushed out of view
at the top is moved to the bottom and its content is updated according to the datasource (code in cellForRow delegate). This is how Reusable Views work. And it is same thing with CollectionView or other objects or custom libraries with Reusable things. So no matter how much data you have, the amount of cells that exist in memory is only 31, at least.



Now that you understand this concept, you can code more efficiently and be able to code safely around it. Before we start to write code for our TableView example, we need to understand another concept that is MVC which means Model-View-Controller. We need to write code that comply with this concept and to do this we have to keep things separate.

The MVC Concept

In case of TableView, our Model is our dataSource. Our View is the TableView object in storyboard obviously. And our Controller is all the delegates functions. Our job is to create our Model that complies to our intention of the View behaviour, and connect Model and View with Controller. In normal English, we need to create our datasource (ie NSArray/NSMutableArray) that has all the necessary properties of the things in our cell and pass it to our delegates.

Lets Do It

Lets make a tableview that lists down a menu for a restaurant with details of the dish in each rows. Such UI is typical in many applications. How do we go about in designing this feature?

We need to create a custom object to work with. Our datasource will be an array containing these objects. So we need to decide what is the contents of our cell item. You can use dictionaries but trust me, objects is the way to go. Objects make coding a lot simpler and cleaner.

So I will split the process into the corresponding "section" of the design pattern:

a) MODEL
b) VIEW
c) CONTROLLER

But we'll start with VIEW first, since it's more fun. :P


Setting Up The "VIEW"

First, drag and drop a UITableView on your empty ViewController. Set the constraints. I leave it up to you how big you want the tableview to be. For me, I just size it up to fill the entire view. And then I drag the UITableViewCell object into the tableview. You should have something like the following.

 Lets review our plan for the tableview cell (which is empty now):
  1. Menu photo - (a UIImageView)
  2. Menu name - (a UILabel)
  3. Price - (another UILabel)
  4. An icon if it is chef recommendation or not. (a UIImageView)
So lets add these items to our empty cell. I made the cell bigger by dragging the bottom edge of the cell downwards. Then adding all the items and arrange them as needed and set the corresponding constraints. I also changed the cell background color just for fun. Here's what I ended up with:


Next, we need to link the cell to our viewController.

If your table contents are simple, like a single text for each cell, you do not need to customize the cell class and you can skip the next step and just use the default cell style class like Basic, Left Detail, Right Detail or Subtitle. 

To customize cell class, right click on your Project Navigator and select New File... then specify the creation of a custom UITableViewCell as shown in image below. I named this custom class MenuTableViewCell:



Now goto Storyboard and click on the tableview cell, and in the Identity Inspector, in Class textfield, key in the name of your custom cell class that you created above. In this case, MenuTableViewCell. This is a crucial step and if you miss it (many noobs do miss it, including me :D), you won't be able to connect the IBOutlet to the class header.



And then, connect all the objects in your cell to the class header. Give it meaningful names. Here is the completed header of the cell class:

//
//  MenuTableViewCell.h
//  Reusables
//
//  Created by Emir Fithri on 03/11/2017.
//  Copyright © 2017 geneCode. All rights reserved.
//

#import 

@interface MenuTableViewCell : UITableViewCell
@property (weak, nonatomic) IBOutlet UIImageView *menuImg;
@property (weak, nonatomic) IBOutlet UILabel *menuName;
@property (weak, nonatomic) IBOutlet UIImageView *menuIcon;
@property (weak, nonatomic) IBOutlet UILabel *menuPrice;

@end

Next, return to storyboard and click on the TableView cell and at the Attributes Inspector key in the Identifier of your custom cell. This is very important. For this example, I named it simply menuCellID. The ID is used by the TableView to distinguish if your table has multiple type of cells styles. Leave MenuTableViewCell.m as it is.

At this point, your custom TableView cell is completed and this concludes setting up the "View" part! Congrats :D



Setting Up The "MODEL"

Our Model is a collection of Objects in NSMutableArray. You could use NSDictionary as objects to store in the array, but custom objects will make coding a whole lot easier and cleaner.


Some options of our Model

So lets code our custom object first. In XCode, right click the project navigator on the left and add a new class. Name it MenuItem. Add the properties according to the details that we are going to add in the cell into the header file. Then create a standard init method for the class in .m file. We will end up as below:

//
//  MenuItem.h
//  Reusables
//
//  Created by Emir Fithri on 03/11/2017.
//  Copyright © 2017 geneCode. All rights reserved.
//

#import <foundation.h>

@interface MenuItem : NSObject

@property (nonatomic, strong) NSString *menuImgName, *menuName, *menuPrice;
@property (nonatomic, assign) int menuCode;


-(id)init;

@end


//
//  MenuItem.m
//  Reusables
//
//  Created by Emir Fithri on 03/11/2017.
//  Copyright © 2017 geneCode. All rights reserved.
//

#import "MenuItem.h"

@implementation MenuItem

-(id)init {
    self = [super init];
    if (self) {
        // you can initialize your properties here if needed
    }
    return self;
}

@end


Now to complete setting up our "Model", we need to declare our NSMutableArray to hold the menu objects, in the viewcontroller's header file:

@property (nonatomic, strong) NSMutableArray *menuArray;

Finally lets create the menu objects and fill them into the menuArray. We can do this in viewDidLoad. Remember to #import "MenuItem.h" in the viewController's header so we can use the class.


-(void)viewDidLoad {     
      [super viewDidLoad];     
    MenuItem *menu1 = [[MenuItem alloc] init];
    menu1.menuName= @"Fish & Chips";
    menu1.menuImgName = @"menu1.jpg";
    menu1.menuPrice = @"12.99";
    menu1.menuCode = 1;
    
    MenuItem *menu2 = [[MenuItem alloc] init];
    menu2.menuName= @"Hamburger";
    menu2.menuImgName = @"menu2.jpg";
    menu2.menuPrice = @"8.99";
    menu2.menuCode = 1;
    
    MenuItem *menu3 = [[MenuItem alloc] init];
    menu3.menuName= @"Beef Steak";
    menu3.menuImgName = @"menu3.jpg";
    menu3.menuPrice = @"21.59";
    menu3.menuCode = 2;

    MenuItem *menu4 = [[MenuItem alloc] init];
    menu4.menuName= @"Cappucino";
    menu4.menuImgName = @"menu4.jpg";
    menu4.menuPrice = @"1.99";
    menu4.menuCode = 1;
    
    MenuItem *menu5 = [[MenuItem alloc] init];
    menu5.menuName= @"Banana Split";
    menu5.menuImgName = @"menu5.jpg";
    menu5.menuPrice = @"11.29";
    menu5.menuCode = 1;
    
    MenuItem *menu6 = [[MenuItem alloc] init];
    menu6.menuName= @"Hot Dog";
    menu6.menuImgName = @"menu6.jpg";
    menu6.menuPrice = @"4.59";
    menu6.menuCode = 2;
    
    MenuItem *menu7 = [[MenuItem alloc] init];
    menu7.menuName= @"Grilled Chicken";
    menu7.menuImgName = @"menu7.jpg";
    menu7.menuPrice = @"25.59";
    menu7.menuCode = 1;
    
    MenuItem *menu8 = [[MenuItem alloc] init];
    menu8.menuName= @"Ice Lemon Tea";
    menu8.menuImgName = @"menu8.jpg";
    menu8.menuPrice = @"1.59";
    menu8.menuCode = 1;

      //initialize our array
      _menuArray = [[NSMutableArray alloc] init];
      [_menuArray addObject:menu1];
      [_menuArray addObject:menu2];
      [_menuArray addObject:menu3];
      [_menuArray addObject:menu4];
      [_menuArray addObject:menu5];
      [_menuArray addObject:menu6];
      [_menuArray addObject:menu7];
      [_menuArray addObject:menu8];
}


In this example, I added only 8 menu items. If you're feeling adventurous, you can go ahead and add as much items as you want. And the code I use is to make you easily understand the process so they are not simplified and not factored in anyway. You can simplify the code for menu item creation by creating your own class method in MenuItem.m, so you can simply call

  [_menuArray addObject:[MenuItem createWithName:@"Beef Steak" photo:@"menu1.jpg" price:@"21.59" code:2]];

I already covered about class methods and functions in previous tutorial: Basic of XCode Methods & Functions in Objective C. Do read it if you want to know more. And finally, of course, we have to add all our actual data (menu images, and icons). You can add them in the Asset folder.


With that, we have completed setting up the "MODEL" part.


Setting Up The "CONTROLLER"

Now here is where the functionality comes in. A Controller for any reusable views basically consists of delegates functions and dataSource link. Some developer or tutorials refers this to "Protocol". It means the same thing. Although, if we want to be concise, we can say that UITableView implements the Protocol, and our class that uses the UITableView's protocol, implements its delegates. You can open up a UITableView.h and see it for yourself:


This is how a class implements a protocol for other class to adopt and use. Get it?

As you probably have noticed, we have not connected our TableView to any IBOutlet yet. Now is the time to do so. And not only that you also have to connect other things on the TableView: In total we need to hook up 3 things:

i) IBOutlet
ii) Datasource
iii) Delegate


The above picture shows how to connect the dataSource of the TableView to the ViewController. Repeat this process for "delegate" underneath it. Then connect the TableView's IBOutlet (New Referencing Outlet) to the ViewController's header. This basically tells the iOS where it can find the delegate functions and the data for this tableView (which is in ViewController).

Next, open up the ViewController's header file and "implement the tableview delegate and datasource".  Basically now we need to add Delegate and Datasource in our class because that's what our UI is looking for when we run the app. To do this, just add <UITableViewDelegate, UITableViewDataSource> in the header file. Your header will now look like this:


//
//  ViewController.h
//  Reusables
//
//  Created by Emir Fithri on 03/11/2017.
//  Copyright © 2017 geneCode. All rights reserved.
//

#import <UIKit.h>
#import "MenuItem.h"

@interface ViewController : UIViewController <UITableViewDelegate, UITableViewDataSource>

@property (nonatomic, strong) NSMutableArray *menuArray;
@property (weak, nonatomic) IBOutlet UITableView *menuTable;

@end

Next goto the ViewController.m file and you can now enter the delegate functions of TableView. You can start to type -(void)table... and you will see code completion pops up a list of all the relevant delegate functions that you can use for the TableView. (If you don't, try hitting ESC button at the end of your typing.)



And that is just a list of delegates that of the type "void". There are delegates of other types too, like NSInteger, CGFloat, and so on. Good news is, you DO NOT HAVE to implement all of these delegate function to make our table work! I'd say the minimum is 3 delegate functions:


// TableView Delegates
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
 
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
       
}

-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
}

The first delegate is to set our cell height. Eventhough we have manually changed the cell size in Storyboard, the tableView will still need to look for this delegate to set the height, otherwise it will use the default size. This is weird behaviour I know. It could probably be a bug, or a feature I am not sure. :D So lets add code to return the cell height.


-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
 return 120.0;
}

Next, we need to tell the TableView how much data we are going to display. As you have added 8 objects to menuArray, we therefore can return this number to the numberOfRowsInSection delegate. How to do it? Simple, just use the count method of the array.

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    
    return [_menuArray count];
}

Important: You should NEVER do calculations in this method. Because it can cause crashes in your app especially when the content of the array you use in cellForRow method does not match with the number you return here. If you want to filter the array, do it outside of delegate functions, load the filtered objects into another array, and return that array's count instead.

And finally, assigning our MODEL to CONTROLLER is using cellForRow method (read the comments to understand what each line does):


-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    // we need to return a cell type to this method, so start with creating our custom cell with the cell identifier we specified earlier
    MenuTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"menuCellID"];
    
    // we also check if the cell is suddenly nil, and we recreate it
    if (cell==nil) {
        cell = [[MenuTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"menuCellID"];
    }
    
    // and here is where the updating according to our model occurs
    // first we get the object of the cell we're in (by using indexPath.row property)
    MenuItem *eachItem = [_menuArray objectAtIndex:indexPath.row];
    
    // then assign the values and data
    cell.menuImg.image = [UIImage imageNamed:eachItem.menuImgName];
    cell.menuPrice.text = eachItem.menuPrice;
    cell.menuName.text = eachItem.menuName;
    
    // as for chef recommended icon, we need a little conditioning
    if (eachItem.menuCode==2) {
        cell.menuIcon.image = [UIImage imageNamed:@"chef.png"];
    } else {
        cell.menuIcon.image = nil; // clear the icon 
    }
    
    // finally we return the updated cell to controller
    return cell;
}

And when you run the app, you will see it works flawlessly! And that's how you use any Reusables. For your own benefit, you can try to apply the same steps to UICollectionView. Try it!



That's all folks! Any questions? Shoot in the comment section. I may be late to reply tho but I'll reply when I can!



No comments:

Post a Comment