O'Reilly logo

iOS 7 Programming Cookbook by Vandad Nahavandipoor

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Chapter 4. Constructing and Using Table Views

4.0. Introduction

A table view is simply a scrolling view that is separated into sections, each of which is further separated into rows. Each row is an instance of the UITableViewCell class, and you can create custom table view rows by subclassing this class.

Using table views is an ideal way to present a list of items to users. You can embed images, text, and other objects into your table view cells; you can customize their height, shape, grouping, and much more. The simplicity of the structure of table views is what makes them highly customizable.

A table view can be fed with data using a table view data source, and you can receive various events and control the physical appearance of table views using a table view delegate object. These are defined, respectively, in the UITableViewDataSource and UITableViewDelegate protocols.

Although an instance of UITableView subclasses UIScrollView, table views can only scroll vertically. This is more a feature than a limitation. In this chapter, we will discuss the different ways of creating, managing, and customizing table views.

Table views can be utilized in two ways:

  • By using the UITableViewController class. This class is similar to the UIViewController class (see Recipe 1.9) in that it is a view controller, but representing a table instead of a normal view. The beauty of this class is that every instance of it already conforms to the UITableViewDelegate and the UITableViewDataSource protocols. So the table view controller by default becomes the data source and the delegate of the table view that it controls. Therefore, in order to implement a method of, for instance, the data source of the table view, all you have to do is implement it in the table view controller instead of having to set the data source of your table view manually to your view controller.

  • By instantiating the UITableView class manually.

Both these methods are valid methods of creating table views. The first method is usually used when you have a table view that fills its container (or the whole screen/window, if the table view controller is the root view controller of the main window of your app). The second method is usually used for situations where you want to display your table view as a smaller part of your UI, perhaps, taking half the width and/or height of the screen. But nothing prevents you from using the second method and setting the width and height of your table view to the width and height of your container window, so that your table view fills the whole screen. We will explore both these methods in this chapter.

Let’s have a look at an example of creating a table view in our application. We are going to see an example of table view controllers in Recipe 4.9, so for now, we will simply focus on creating table views in code and adding them to an existing view controller.

The way to instantiate UITableView is through its initWithFrame:style: method. Let’s see what parameters we have to pass to this method and what those parameters mean:

initWithFrame

This is a parameter of type CGRect. This specifies the where the table view has to be positioned in its super view. If you want your table view to simply cover your whole view, pass the value of the bounds property of your view controller’s view to this parameter.

style

This is a parameter of type UITableViewStyle that is defined in this way:

typedef NS_ENUM(NSInteger, UITableViewStyle) {
    UITableViewStylePlain,
    UITableViewStyleGrouped
};

Figure 4-1 shows the difference between a plain table view and a grouped table view.

Different types of table views
Figure 4-1. Different types of table views

We feed data to a table view using its data source, as we will see in Recipe 4.1. Table views also have delegates that receive various events from the table view. Delegate objects have to conform to the UITableViewDelegate protocol. There are some methods in this protocol that are quite important to know:

tableView:viewForHeaderInSection:

Gets called on the delegate when the table view wants to render the header view of a section. Each section of a table view can contain a header, some cells, and a footer. We will talk about all these in this chapter. The header and footer are simple instances of UIView. This method is optional, but if you want to configure a header for your table view sections, use this method to create that instance of the view and return it as the return value. To read more about headers and footers in table views, refer to Recipe 4.5.

tableView:viewForFooterInSection:

Same as the tableView:viewForHeaderInSection: delegate method, but returns the footer view. Like the header, the footer is optional but should be created here if you want one. To read more about headers and footers in table views, refer to Recipe 4.5.

tableView:didEndDisplayingCell:forRowAtIndexPath:

Gets called on your delegate object when a cell is scrolled off the screen. This is a really handy method to have called on our delegate because you can delete objects and remove them from memory, if those objects were associated with the cell that is scrolled off the screen and you expect that you may no longer need them.

tableView:willDisplayCell:forRowAtIndexPath:

This method is called on the delegate of a table view whenever a cell is about to be displayed on the screen.

You can set the delegate of a table view simply by setting the value of the delegate property of an instance of UITableView to an object that conforms to the UITableViewDelegate protocol. If your table view is part of a view controller, you can simply make your view controller the delegate of your table view, like so:

#import "ViewController.h"

@interface ViewController () <UITableViewDelegate>
@property (nonatomic, strong) UITableView *myTableView;
@end

@implementation ViewController

- (void)viewDidLoad{
    [super viewDidLoad];

    self.myTableView = [[UITableView alloc]
                        initWithFrame:self.view.bounds
                        style:UITableViewStylePlain];

    self.myTableView.delegate = self;

    [self.view addSubview:self.myTableView];

}

@end

Think of the delegate of a table view as an object that listens to various events sent by the table view, such as when a cell is selected or when the table view wants to figure out the height of each of its cells.

Note

It is mandatory for the delegate object to respond to messages that are marked as @required by the UITableViewDelegate protocol. Responding to other messages is optional, but the delegate must respond to any messages you want to affect the table view.

Messages sent to the delegate object of a table view carry a parameter that tells the delegate object which table view has fired that message in its delegate. This is very important to note because you might, under certain circumstances, require more than one table view to be placed on one object (usually a view). Because of this, it is highly recommended that you make your decisions based on which table view has actually sent that specific message to your delegate object, like so:

- (CGFloat)     tableView:(UITableView *)tableView
  heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    if ([tableView isEqual:self.myTableView]){
        return 100.0f;
    }
    return 40.0f;
}

The location of a cell in a table view is represented by its index path. An index path is the combination of the section and the row index, where the section index is the zero-based index specifying which grouping or section each cell belongs to, and the cell index is the zero-based index of that particular cell in its section.

4.1. Populating a Table View with Data

Problem

You would like to populate your table view with data.

Solution

Conform to the UITableViewDataSource protocol in an object and assign that object to the dataSource property of a table view.

Discussion

Create an object that conforms to the UITableViewDataSource protocol and assign it to a table view instance. Then, by responding to the data source messages, provide information to your table view. For this example, let’s go ahead and declare the .m file of our view controller, which will later create a table view on its own view, in code:

#import "ViewController.h"

static NSString *TableViewCellIdentifier = @"MyCells";

@interface ViewController () <UITableViewDataSource>
@property (nonatomic, strong) UITableView *myTableView;
@end

The TableViewCellIdentifier contains our cell identifiers as a static string variable. Each cell, as you will learn soon, can have an identifier, which is great for reusing cells. For now, think about this as a unique identifier for all the cells in our table view, nothing more.

In the viewDidLoad method of our view controller, we create the table view and assign our view controller as its data source:

- (void)viewDidLoad{
    [super viewDidLoad];

    self.myTableView =
    [[UITableView alloc] initWithFrame:self.view.bounds
                                 style:UITableViewStylePlain];

    [self.myTableView registerClass:[UITableViewCell class]
             forCellReuseIdentifier:TableViewCellIdentifier];

    self.myTableView.dataSource = self;

    /* Make sure our table view resizes correctly */
    self.myTableView.autoresizingMask =
        UIViewAutoresizingFlexibleWidth |
        UIViewAutoresizingFlexibleHeight;

    [self.view addSubview:self.myTableView];

}

Everything is very simple in this code snippet except for the registerClass:forCellReuseIdentifier: method that we are calling on the instance of our table view. What does this method do, you ask? The registerClass parameter of this method simply takes a class name that denotes the type of object that you want your table view to load, when it renders each cell. Cells inside a table view all have to be direct or indirect ancestors of the UITableViewCell class. This class on its own provides a lot of functionalities to programmers, but if you want to extend this class, you can simply subclass it and add your new functionalities to your own class. So going back to the registerClass parameter of the aforementioned method, you have to pass the class name of your cells to this parameter and then pass an identifier to the forCellReuseIdentifier parameter. The reason behind associating table view cell classes with identifiers is that later, when you populate your table view, you can simply pass the same identifier to the table view’s dequeueReusableCellWithIdentifier:forIndexPath: method and have the table view instantiate the cell for you if one cannot be reused. This is great stuff, because in previous versions of the iOS SDK, programmers had to instantiate these cells themselves if a previous and reusable cell could not be retrieved from the table view.

Now we need to make sure our table view responds to the @required methods of the UITableViewDataSource protocol. Press the Command+Shift+O key combination on your keyboard, type this protocol name in the dialog, and then press the Enter key on your keyboard. This will show you the required methods for this protocol.

The UITableView class defines a property called dataSource. This is an untyped object that must conform to the UITableViewDataSource protocol. Every time a table view is refreshed and reloaded using the reloadData method, the table view will call various methods in its data source to find out about the data you intend to populate it with. A table view data source can implement three important methods, two of which are mandatory for every data source:

numberOfSectionsInTableView:

This method allows the data source to inform the table view of the number of sections that must be loaded into the table.

tableView:numberOfRowsInSection:

This method tells the view controller how many cells or rows have to be loaded for each section. The section number is passed to the data source in the numberOfRowsInSection parameter. The implementation of this method is mandatory in the data source object.

tableView:cellForRowAtIndexPath:

This method is responsible for returning instances of the UITableViewCell class as rows that have to be populated into the table view. The implementation of this method is mandatory in the data source object.

So let’s go ahead and implement these methods in our view controller, one by one. First, let’s tell the table view that we want it to render three sections:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{

    if ([tableView isEqual:self.myTableView]){
        return 3;
    }

    return 0;

}

Then we tell the table view how many rows we want it to render, for each section:

- (NSInteger)tableView:(UITableView *)tableView
 numberOfRowsInSection:(NSInteger)section{

    if ([tableView isEqual:self.myTableView]){
        switch (section){
            case 0:{
                return 3;
                break;
            }
            case 1:{
                return 5;
                break;
            }
            case 2:{
                return 8;
                break;
            }
        }
    }

    return 0;

}

So up to now, we have asked the table view to render three sections with three rows in the first, five rows in the second, and eight rows in the third section. What’s next? We have to return instances of UITableViewCell to the table view—the cells that we want the table view to render:

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

    UITableViewCell *cell = nil;

    if ([tableView isEqual:self.myTableView]){

        cell = [tableView
                  dequeueReusableCellWithIdentifier:TableViewCellIdentifier
                  forIndexPath:indexPath];

        cell.textLabel.text = [NSString stringWithFormat:
                                 @"Section %ld, Cell %ld",
                                 (long)indexPath.section,
                                 (long)indexPath.row];

    }

    return cell;

}

Now if we run our app in iPhone Simulator, we will see the results of our work (Figure 4-2).

A plain table view with three sections
Figure 4-2. A plain table view with three sections

When a table view is reloaded or refreshed, it queries its data source through the UITableViewDataSource protocol, asking for various bits of information. Among the important methods previously mentioned, the table view will first ask for the number of sections. Each section is responsible for holding rows or cells. After the data source specifies the number of sections, the table view will ask for the number of rows that have to be loaded into each section. The data source gets the zero-based index of each section and, based on this, can decide how many cells have to be loaded into each section.

The table view, after determining the number of cells in the sections, will continue to ask the data source about the view that will represent each cell in each section. You can allocate instances of the UITableViewCell class and return them to the table view. There are, of course, properties that can be set for each cell, including the title, subtitle, and color of each cell, among other properties.

4.2. Using Different Types of Accessories in a Table View Cell

Problem

You want to grab users’ attention in a table view by displaying accessories, and offer different ways to interact with each cell in your table view.

Solution

Use the accessoryType of the UITableViewCell class, instances of which you provide to your table view in its data source object:

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

    UITableViewCell* result = nil;

    if ([tableView isEqual:self.myTableView]){

        result = [tableView
                  dequeueReusableCellWithIdentifier:MyCellIdentifier
                  forIndexPath:indexPath];

        result.textLabel.text =
        [NSString stringWithFormat:@"Section %ld, Cell %ld",
         (long)indexPath.section,
         (long)indexPath.row];

        result.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;

    }

    return result;

}

- (NSInteger) tableView:(UITableView *)tableView
  numberOfRowsInSection:(NSInteger)section{
    return 10;
}

- (void)viewDidLoad{
    [super viewDidLoad];

    self.myTableView = [[UITableView alloc]
                        initWithFrame:self.view.bounds
                        style:UITableViewStylePlain];

    [self.myTableView registerClass:[UITableViewCell class]
             forCellReuseIdentifier:MyCellIdentifier];

    self.myTableView.dataSource = self;

    self.myTableView.autoresizingMask =
        UIViewAutoresizingFlexibleWidth |
        UIViewAutoresizingFlexibleHeight;

    [self.view addSubview:self.myTableView];

}

Discussion

You can assign any of the values defined in the UITableViewCellAccessoryType enumeration to the accessoryType property of an instance of the UITableViewCell class. Two very useful accessories are the disclosure indicator and the detail disclosure button. They both display a chevron indicating to users that if they tap on the associated table view cell, a new view or view controller will be displayed. In other words, the users will be taken to a new screen with further information about their current selector. The difference between these two accessories is that the disclosure indicator produces no event, whereas the detail disclosure button fires an event to the delegate when pressed. In other words, pressing the button has a different effect from pressing the cell itself. Thus, the detail disclosure button allows the user to perform two separate but related actions on the same row.

Figure 4-3 shows these two different accessories on a table view. The first row has a disclosure indicator and the second row has a detail disclosure button.

Two table view cells with different accessories
Figure 4-3. Two table view cells with different accessories

If you tap any detail disclosure button assigned to a table view cell, you will immediately realize that it truly is a separate button. Now the question is: how does the table view know when the user taps this button?

Table views, as explained before, fire events on their delegate object. The detail disclosure button on a table view cell also fires an event that can be captured by the delegate object of a table view:

- (void)                        tableView:(UITableView *)tableView
 accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath{

    /* Do something when the accessory button is tapped */
    NSLog(@"Accessory button is tapped for cell at index path = %@", indexPath);

    UITableViewCell *ownerCell = [tableView cellForRowAtIndexPath:indexPath];

    NSLog(@"Cell Title = %@", ownerCell.textLabel.text);

}

This code finds the table view cell whose detail disclosure button has been tapped and prints the contents of the text label of that cell into the console screen. As a reminder, you can display the console screen in Xcode by selecting Run Console.

4.3. Creating Custom Table View Cell Accessories

Problem

The accessories provided to you by the iOS SDK are not sufficient, and you would like to create your own accessories.

Solution

Assign an instance of the UIView class to the accessoryView property of any instance of the UITableViewCell class:

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

    UITableViewCell* cell = nil;

    cell = [tableView dequeueReusableCellWithIdentifier:MyCellIdentifier
                                           forIndexPath:indexPath];

    cell.textLabel.text = [NSString stringWithFormat:@"Section %ld, Cell %ld",
                             (long)indexPath.section,
                             (long)indexPath.row];

    UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
    button.frame = CGRectMake(0.0f, 0.0f, 150.0f, 25.0f);

    [button setTitle:@"Expand"
            forState:UIControlStateNormal];

    [button addTarget:self
               action:@selector(performExpand:)
     forControlEvents:UIControlEventTouchUpInside];

    cell.accessoryView = button;

    return cell;

}

As you can see, this code uses the performExpand: method as the selector for each button. Here is the definition of this method:

- (void) performExpand:(UIButton *)paramSender{

    /* Handle the tap event of the button */

}

This example code snippet assigns a custom button to the accessory view of every row in the targeted table. The result is shown in Figure 4-4.

Table view cells with custom accessory views
Figure 4-4. Table view cells with custom accessory views

Discussion

An object of type UITableViewCell retains a property named accessoryView. This is the view you can assign a value to if you are not completely happy with the built-in iOS SDK table view cell accessories. After this property is set, Cocoa Touch will ignore the value of the accessoryType property and will use the view assigned to the accessoryView property as the accessory assigned to the cell.

The code listed in this recipe’s Solution creates buttons for all the cells populated into the table view. When a button is pressed in any cell, the performExpand: method gets called, and if you are like me, you have probably already started thinking about how you can determine which cell the sender button belongs to. So, now we have to somehow link our buttons with the cells to which they belong.

One way to handle this situation is to take advantage of the tag property of the button instance. The tag property is a simple integer that people usually use to associate a view with another object. For instance, if you want to associate the button with the third cell in your table view, set the value of the button’s tag property to 3. But there is a problem here: table views have sections, and every section can have n number of cells. We, therefore, have to be able to determine the section as well as the cell that owns our button, and since the tag can only represent one integer, this makes things more difficult. Instead of a tag, therefore, we can ask for the superview of the accessory view, going recursively up the chain of views until we find the cell of type UITableViewCell, like so:

- (UIView *) superviewOfType:(Class)paramSuperviewClass
                     forView:(UIView *)paramView{

    if (paramView.superview != nil){
        if ([paramView.superview isKindOfClass:paramSuperviewClass]){
            return paramView.superview;
        } else {
            return [self superviewOfType:paramSuperviewClass
                                 forView:paramView.superview];
        }
    }

    return nil;

}

- (void) performExpand:(UIButton *)paramSender{

    /* Handle the tap event of the button */
    __unused UITableViewCell *parentCell =
        (UITableViewCell *)[self superviewOfType:[UITableViewCell class]
                                         forView:paramSender];

    /* Now do something with the cell if you want to */

}

This is a simple recursive method that accepts a view (in this case our button) and a class name (in this case, UITableViewCell), then searches in the view’s super view hierarchy to find the super view that is of the given class. So it starts with the super view of the given view, and if that super view is not of the required type, looks at the super view’s super view, and so on until it finds the super view of the requested class. You can see that we are using the Class structure as the first parameter to the superviewOfType:forView: method. This data type can hold any Objective-C class name and it’s great if you are looking for or asking for specific class names from the programmer.

4.4. Enabling Swipe Deletion of Table View Cells

Problem

You want your application users to be able to delete rows from a table view easily.

Solution

Implement the tableView:editingStyleForRowAtIndexPath: selector in the delegate and the tableView:commitEditingStyle:forRowAtIndexPath: selector in the data source of your table view:

- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView
           editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath{

    return UITableViewCellEditingStyleDelete;

}

- (void) setEditing:(BOOL)editing
           animated:(BOOL)animated{

    [super setEditing:editing
             animated:animated];

    [self.myTableView setEditing:editing
                        animated:animated];


}

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

    if (editingStyle == UITableViewCellEditingStyleDelete){

        /* First remove this object from the source */
        [self.allRows removeObjectAtIndex:indexPath.row];

        /* Then remove the associated cell from the Table View */
        [tableView deleteRowsAtIndexPaths:@[indexPath]
                         withRowAnimation:UITableViewRowAnimationLeft];

    }

}

The tableView:editingStyleForRowAtIndexPath: method can enable deletions. It is called by the table view and its return value determines what the table view allows the user to do (insertion, deletion, etc.). The tableView:commitEditingStyle:forRowAtIndexPath: method carries out the user’s requested deletion. The latter method is defined in the delegate, but its functionality is a bit overloaded: not only do you use the method to delete data, but you also have to delete rows from the table here.

Discussion

The table view responds to the swipe by showing a button on the right side of the targeted row (Figure 4-5). As you can see, the table view is not in editing mode, but the button allows the user to delete the row.

This mode is enabled by implementing the tableView:editingStyleForRowAtIndexPath: method (declared in the UITableViewDelegate protocol), whose return value indicates whether the table should allow insertions, deletions, both, or neither. By implementing the tableView:commitEditingStyle:forRowAtIndexPath: method in the data source of a table view, you can then get notified if a user has performed an insertion or deletion.

Delete button appearing on a table view cell
Figure 4-5. Delete button appearing on a table view cell

The second parameter of the deleteRowsAtIndexPaths:withRowAnimation: method allows you to specify an animation method that will be performed when rows are deleted from a table view. Our example specifies that we want rows to disappear by moving from right to left when deleted.

4.5. Constructing Headers and Footers in Table Views

Problem

You want to create a header and/or a footer for a table view.

Solution

Create a view (could be a label, image view, etc., anything that directly or indirectly subclasses UIView), and assign that view to the header and/or the footer of a section of a table view. You can also allocate specific number of points in height for a header or a footer, as we will soon see.

Discussion

A table view can have multiple headers and footers. Each section in a table view can have its own header and footer, so if you have three sections in a table view, you can have a maximum of three headers and maximum of three footers. You are not obliged to provide headers and footers for any of these sections. It is up to you to tell the table view whether you want a header and/or a footer for a section and you pass these views to the table view through its delegate, should you wish to provide header(s)/footer(s) for section(s) of your table view. Headers and footers in a table view become a part of the table view, meaning that when the table view’s contents scroll, so do the header(s) and footer(s) inside that table view. Let’s have a look at a sample header and footer in a table view (Figure 4-6).

A footer for the top section and the Shortcuts header for the last section of a table view
Figure 4-6. A footer for the top section and the Shortcuts header for the last section of a table view

As you can see, the top section (with items such as “Check Spelling” and “Enable Caps Lock”) has a footer that says “Double tapping the space bar will insert a period followed by a space.” That is the footer of the top section of that table view. The reason why it is a footer rather than a header is because it is attached to the bottom of that section rather than the top. The last section in this table view also has a header that reads “SHORTCUTS.” The reason why this is a header rather than a footer is because it appears on the top of the section rather than the bottom.

Note

Specifying the height of a header and footer in a section inside a table view is done through methods defined in the UITableViewDataSource. Specifying the actual view that has to be displayed for the header/footer of a section in a table view is done through methods defined in the UITableViewDelegate protocol.

Let’s go ahead and create a simple app with one table view in it. Then let’s provide two labels, of type UILabel, one as the header and the other as the footer of the only section in our table view, and populate this one section with only three cells. In the header we will place the text “Section 1 Header,” and in the footer label we will place the text “Section 1 Footer.” Starting with the implementation file of our root view controller, we will define a table view:

#import "ViewController.h"

static NSString *CellIdentifier = @"CellIdentifier";

@interface ViewController () <UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, strong) UITableView *myTableView;
@end

@implementation ViewController

Now we will create a grouped table view and load three cells into it:

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

    UITableViewCell *cell = nil;

    cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier
                                           forIndexPath:indexPath];

    cell.textLabel.text = [[NSString alloc] initWithFormat:@"Cell %ld",
                             (long)indexPath.row];

    return cell;

}

- (NSInteger) tableView:(UITableView *)tableView
  numberOfRowsInSection:(NSInteger)section{
    return 3;
}

- (void)viewDidLoad{
    [super viewDidLoad];

    self.myTableView =
    [[UITableView alloc] initWithFrame:self.view.bounds
                                 style:UITableViewStyleGrouped];

    [self.myTableView registerClass:[UITableViewCell class]
             forCellReuseIdentifier:CellIdentifier];

    self.myTableView.dataSource = self;
    self.myTableView.delegate = self;
    self.myTableView.autoresizingMask = UIViewAutoresizingFlexibleWidth |
    UIViewAutoresizingFlexibleHeight;

    [self.view addSubview:self.myTableView];

}

Here is the exciting part. We can now use two important methods (which are defined in UITableViewDelegate) to provide a label for the header and another label for the footer of the one section that we have loaded into our table view. These methods are:

tableView:viewForHeaderInSection:

This method expects a return value of type UIView. The view returned from this method will be displayed as the header of the section specified by the viewForHeaderInSection parameter.

tableView:viewForFooterInSection:

This method expects a return value of type UIView. The view returned from this method will be displayed as the footer of the section specified by the viewForFooterInSection parameter.

Our task now is to implement these methods and return an instance of UILabel. On the header label we will enter the text “Section 1 Header,” and on the footer label the text “Section 1 Footer,” as we had planned:

- (UILabel *) newLabelWithTitle:(NSString *)paramTitle{
    UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
    label.text = paramTitle;
    label.backgroundColor = [UIColor clearColor];
    [label sizeToFit];
    return label;
}

- (UIView *)  tableView:(UITableView *)tableView
 viewForHeaderInSection:(NSInteger)section{

    if (section == 0){
        return [self newLabelWithTitle:@"Section 1 Header"];
    }

    return nil;

}

- (UIView *)  tableView:(UITableView *)tableView
 viewForFooterInSection:(NSInteger)section{

    if (section == 0){
        return [self newLabelWithTitle:@"Section 1 Footer"];
    }

    return nil;

}

If you run your app on the iOS Simulator now, you will certainly see something strange, as shown in Figure 4-7.

The header and footer labels of a table view are not aligned properly
Figure 4-7. The header and footer labels of a table view are not aligned properly

The reason for this misalignment of the labels and the omission of the header is that the table view doesn’t really know the height of these views. To specify the height of the header and footer views, you need to use the following two methods, which are defined in the UITableViewDelegate protocol:

tableView:heightForHeaderInSection:

The return value of this method, of type CGFloat, specifies the height of the header for a section in a table view. The section’s index is passed through the heightForHeaderInSection parameter.

tableView:heightForFooterInSection:

The return value of this method, of type CGFloat, specifies the height of the footer for a section in a table view. The section’s index is passed through the heightForHeaderInSection parameter.

- (CGFloat)     tableView:(UITableView *)tableView
 heightForHeaderInSection:(NSInteger)section{

    if (section == 0){
        return 30.0f;
    }

    return 0.0f;
}

- (CGFloat)     tableView:(UITableView *)tableView
 heightForFooterInSection:(NSInteger)section{

    if (section == 0){
        return 30.0f;
    }

    return 0.0f;

}

Running the app, you can see that the height of the header and the footer labels is fixed. There is still something wrong with the code we’ve written—the left margin of our header and footer labels. Take a look for yourself in Figure 4-8.

The left margin of our header and footer labels is not correct
Figure 4-8. The left margin of our header and footer labels is not correct

The reason for this is that the table view, by default, places header and footer views at x point 0.0f. You might think that changing the frame of your header and footer labels will fix this issue, but unfortunately it doesn’t. The solution to this problem is creating a generic UIView and placing your header and footer labels on that view. Return the generic view as the header/footer, but change the x position of your labels within the generic view. We now need to modify our implementation of the tableView:viewForHeaderInSection: and the tableView:viewForFooterInSection: methods:

- (UIView *)  tableView:(UITableView *)tableView
 viewForHeaderInSection:(NSInteger)section{

    UIView *header = nil;

    if (section == 0){

        UILabel *label = [self newLabelWithTitle:@"Section 1 Header"];

        /* Move the label 10 points to the right */
        label.frame = CGRectMake(label.frame.origin.x + 10.0f,
                                 5.0f, /* Go 5 points down in y axis */
                                 label.frame.size.width,
                                 label.frame.size.height);

        /* Give the container view 10 points more in width than our label
         because the label needs a 10 extra points left-margin */
        CGRect resultFrame = CGRectMake(0.0f,
                                        0.0f,
                                        label.frame.size.width + 10.0f,
                                        label.frame.size.height);
        header = [[UIView alloc] initWithFrame:resultFrame];
        [header addSubview:label];

    }

    return header;

}

- (UIView *)  tableView:(UITableView *)tableView
 viewForFooterInSection:(NSInteger)section{

    UIView *footer = nil;

    if (section == 0){

        UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];

        /* Move the label 10 points to the right */
        label.frame = CGRectMake(label.frame.origin.x + 10.0f,
                                 5.0f, /* Go 5 points down in y axis */
                                 label.frame.size.width,
                                 label.frame.size.height);

        /* Give the container view 10 points more in width than our label
         because the label needs a 10 extra points left-margin */
        CGRect resultFrame = CGRectMake(0.0f,
                                        0.0f,
                                        label.frame.size.width + 10.0f,
                                        label.frame.size.height);
        footer = [[UIView alloc] initWithFrame:resultFrame];
        [footer addSubview:label];

    }

    return footer;

}

Now if you run your app, you will get results similar to Figure 4-9.

Our header and footer labels displayed in a table view
Figure 4-9. Our header and footer labels displayed in a table view

With the methods you just learned, you can even place images as the header/footer of your table views. Instances of UIImageView have UIView as their superclass, so you can easily place your images in image views and return them as headers/footers of a table view. If all you want to place is text as the header/footer of table views, you can use two handy methods defined in the UITableViewDataSource protocol, which will save you a lot of hassle. Instead of creating your own labels and returning them as headers/footers of your table view, you can simply use these methods:

tableView:titleForHeaderInSection:

The return value of this method is of type NSString. This string will automatically be placed inside a label by the table view and will be displayed as the header of the section, which is specified in the titleForHeaderInSection parameter.

tableView:titleForFooterInSection:

The return value of this method is of type NSString. This string will automatically be placed inside a label by the table view and will be displayed as the footer of the section, which is specified in the titleForFooterInSection parameter.

So to make our app’s code simpler, let’s get rid of our implementation of the tableView:viewForHeaderInSection: and the tableView:viewForFooterInSection: methods, and replace them with the implementation of the tableView:titleForHeaderInSection: and the tableView:titleForFooterInSection: methods:

- (NSString *) tableView:(UITableView *)tableView
 titleForHeaderInSection:(NSInteger)section{

    if (section == 0){
        return @"Section 1 Header";
    }

    return nil;

}

- (NSString *) tableView:(UITableView *)tableView
 titleForFooterInSection:(NSInteger)section{

    if (section == 0){
        return @"Section 1 Footer";
    }

    return nil;

}

Now run your app in the iPhone Simulator, and you will see that the table view has automatically created a left-aligned label for the header and the footer of the only section in our table view. In iOS 7, by default, the header and the footer are left-aligned. In earlier versions of iOS, the header is left aligned but the footer is center aligned. In every version, the table view can set the alignment of these labels (see Figure 4-10).

A table view rendering text in headers and footers
Figure 4-10. A table view rendering text in headers and footers

4.6. Displaying Context Menus on Table View Cells

Problem

You want to give your users the ability to use copy/paste options among other operations that they can choose, by holding down one of their fingers on a table view cell in your app.

Solution

Implement the following three methods of the UITableViewDelegate protocol in the delegate object of your table view:

tableView:shouldShowMenuForRowAtIndexPath:

The return value of this method is of type BOOL. If you return YES from this method, iOS will display the context menu for the table view cell whose index gets passed to you through the shouldShowMenuForRowAtIndexPath parameter.

tableView:canPerformAction:forRowAtIndexPath:withSender:

The return value of this method is also of type BOOL. Once you allow iOS to display a context menu for a table view cell, iOS will call this method multiple times and pass you the selector of the action that you can choose to display in the context menu or not. So, if iOS wants to ask you whether you would like to show the Copy menu to be displayed to the user, this method will get called in your table view’s delegate object and the canPerformAction parameter of this method will be equal to @selector(copy:). We will read more information about this in this recipe’s Discussion.

tableView:performAction:forRowAtIndexPath:withSender:

Once you allow a certain action to be displayed in the context menu of a table view cell, when the user picks that action from the menu, this method will get called in your table view’s delegate object. In here, you must do whatever needs to be done to satisfy the user’s request. For instance, if it is the Copy menu that the user has selected, you will need to use a pasteboard to place the chosen table view cell’s content into the pasteboard.

Discussion

A table view can give a yes/no answer to iOS, allowing or disallowing the display of available system menu items for a table view cell. iOS attempts to display a context menu on a table view cell when the user has held down his finger on the cell for a certain period of time, roughly about one second. iOS then asks the table view whose cell was the source of the trigger for the menu. If the table view gives a yes answer, iOS will then tell the table view what options can be displayed in the context menu, and the table view will be able to say yes or no to any of those items. If there are five menu items available, for instance, and the table view says yes to only two of them, then only those two items will be displayed.

After the menu items are displayed to the user, the user can either tap on one of the items or tap outside the context menu to cancel it. Once the user taps on one of the menu items, iOS will send a delegate message to the table view informing it of the menu item that the user has picked. Based on this information, the table view can make a decision as to what to do with the selected action.

I suggest that we first see what actions are actually available for a context menu on a table view cell, so let’s create our table view and then display a few cells inside it:

- (NSInteger) tableView:(UITableView *)tableView
  numberOfRowsInSection:(NSInteger)section{
    return 3;
}

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

    UITableViewCell *cell = nil;

    cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier
                                           forIndexPath:indexPath];

    cell.textLabel.text = [[NSString alloc]
                             initWithFormat:@"Section %ld Cell %ld",
                             (long)indexPath.section,
                             (long)indexPath.row];

    return cell;

}

- (void)viewDidLoad{
    [super viewDidLoad];

    self.myTableView = [[UITableView alloc]
                        initWithFrame:self.view.bounds
                        style:UITableViewStylePlain];

    [self.myTableView registerClass:[UITableViewCell class]
             forCellReuseIdentifier:CellIdentifier];

    self.myTableView.autoresizingMask =
        UIViewAutoresizingFlexibleWidth |
        UIViewAutoresizingFlexibleHeight;

    self.myTableView.dataSource = self;
    self.myTableView.delegate = self;

    [self.view addSubview:self.myTableView];

}

Now we will implement the three aforementioned methods defined in the UITableViewDelegate protocol and simply convert the available actions (of type SEL) to strings and print them out to the console:

- (BOOL)                tableView:(UITableView *)tableView
  shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath{

    /* Allow the context menu to be displayed on every cell */
    return YES;

}

- (BOOL) tableView:(UITableView *)tableView
  canPerformAction:(SEL)action
 forRowAtIndexPath:(NSIndexPath *)indexPath
        withSender:(id)sender{

    NSLog(@"%@", NSStringFromSelector(action));

    /* Allow every action for now */
    return YES;
}

- (void) tableView:(UITableView *)tableView
     performAction:(SEL)action
 forRowAtIndexPath:(NSIndexPath *)indexPath
        withSender:(id)sender{

    /* Empty for now */

}

Now run your app in the simulator or on the device. You will then see three cells loaded into the table view. Hold down your finger (if on a device) or your pointer (if using iOS Simulator) on one of the cells and observe what gets printed out to the console window:

cut:
copy:
select:
selectAll:
paste:
delete:
_promptForReplace:
_showTextStyleOptions:
_define:
_addShortcut:
_accessibilitySpeak:
_accessibilitySpeakLanguageSelection:
_accessibilityPauseSpeaking:
makeTextWritingDirectionRightToLeft:
makeTextWritingDirectionLeftToRight:

These are all the actions that iOS will allow you to show your users, should you need them. So for instance, if you would like to allow your users to have the Copy option, in the tableView:canPerformAction:forRowAtIndexPath:withSender: method, simply find out which action iOS is asking your permission for before displaying it, and either return YES or NO:

- (BOOL) tableView:(UITableView *)tableView
  canPerformAction:(SEL)action
 forRowAtIndexPath:(NSIndexPath *)indexPath
        withSender:(id)sender{

    if (action == @selector(copy:)){
        return YES;
    }

    return NO;
}

The next step is to intercept what menu item the user actually selected from the context menu. Based on this information, we can then take appropriate action. For instance, if the user selected the Copy item in the context menu (see Figure 4-11), then we can use UIPasteBoard to copy that cell into the pasteboard for later use:

- (void) tableView:(UITableView *)tableView
     performAction:(SEL)action
 forRowAtIndexPath:(NSIndexPath *)indexPath
        withSender:(id)sender{

    if (action == @selector(copy:)){

        UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
        UIPasteboard *pasteBoard = [UIPasteboard generalPasteboard];
        [pasteBoard setString:cell.textLabel.text];

    }

}
The Copy action displayed inside a context menu on a table view cell
Figure 4-11. The Copy action displayed inside a context menu on a table view cell

4.7. Moving Cells and Sections in Table Views

Problem

You want to move and shuffle cells and sections inside a table view, with smooth and intuitive animations.

Solution

Use the moveSection:toSection: method of the table view to move a section to a new position. You can also use the moveRowAtIndexPath:toIndexPath: method to move a table view cell from its current place to a new place.

Discussion

Moving table view cells and sections differs from exchanging them. Let’s have a look at an example that will make this easier to understand. Let’s say you have three sections in your table view: Sections A, B, and C. If you move Section A to Section C, the table view will notice this move and will then shift Section B to the previous position of Section A, and will move Section B to the previous position of Section B. However, if Section B is moved to Section C, the table view will not have to move Section A at all, as it is sitting on top and doesn’t interfere with the repositioning of Section B and C. In this case, Section B will be moved to Section C and Section C to Section B. The same logic will be used by the table view when moving cells.

To demonstrate this, let’s create a table view and preload it with three sections, each of which contains three cells of its own. Let’s start with the implementation file of our view controller:

#import "ViewController.h"

static NSString *CellIdentifier = @"CellIdentifier";

@interface ViewController () <UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, strong) UITableView *myTableView;
@property (nonatomic, strong) NSMutableArray *arrayOfSections;
@end

Our view controller will become the data source of the table view. The table view has sections and each section has rows. We will keep an array of arrays; the first array is our array of sections, which will itself contain other arrays that contain our cells. The arrayOfSections defined on top of the implementation file of our view controller will bear that responsibility. Let’s go ahead and populate this array:

- (NSMutableArray *) newSectionWithIndex:(NSUInteger)paramIndex
                               cellCount:(NSUInteger)paramCellCount{

    NSMutableArray *result = [[NSMutableArray alloc] init];

    NSUInteger counter = 0;
    for (counter = 0;
         counter < paramCellCount;
         counter++){

        [result addObject:[[NSString alloc] initWithFormat:@"Section %lu Cell %lu",
                           (unsigned long)paramIndex,
                           (unsigned long)counter+1]];

    }

    return result;

}

- (NSMutableArray *) arrayOfSections{
    if (_arrayOfSections == nil){
        NSMutableArray *section1 = [self newSectionWithIndex:1
                                                   cellCount:3];
        NSMutableArray *section2 = [self newSectionWithIndex:2
                                                   cellCount:3];
        NSMutableArray *section3 = [self newSectionWithIndex:3
                                                   cellCount:3];

        _arrayOfSections = [[NSMutableArray alloc] initWithArray:@[
                                                                   section1,
                                                                   section2,
                                                                   section3
                                                                   ]
                            ];
    }
    return _arrayOfSections;
}

We shall then instantiate our table view and implement the necessary methods in the UITableViewDataSource protocol to populate our table view with data:

- (NSInteger) numberOfSectionsInTableView:(UITableView *)tableView{

    return self.arrayOfSections.count;

}

- (NSInteger) tableView:(UITableView *)tableView
  numberOfRowsInSection:(NSInteger)section{

    NSMutableArray *sectionArray = self.arrayOfSections[section];
    return sectionArray.count;

}

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

    UITableViewCell *cell = nil;


    cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier
                                           forIndexPath:indexPath];


    NSMutableArray *sectionArray = self.arrayOfSections[indexPath.section];

    cell.textLabel.text = sectionArray[indexPath.row];

    return cell;

}

- (void)viewDidLoad{
    [super viewDidLoad];

    self.myTableView =
    [[UITableView alloc] initWithFrame:self.view.bounds
                                 style:UITableViewStyleGrouped];

    [self.myTableView registerClass:[UITableViewCell class]
             forCellReuseIdentifier:CellIdentifier];

    self.myTableView.autoresizingMask =
        UIViewAutoresizingFlexibleWidth |
        UIViewAutoresizingFlexibleHeight;

    self.myTableView.delegate = self;
    self.myTableView.dataSource = self;

    [self.view addSubview:self.myTableView];

}

Show time! Shall we first have a look at how sections can be moved to a new position? Let’s write a method that will move Section 1 to 3:

- (void) moveSection1ToSection3{

    NSMutableArray *section1 = self.arrayOfSections[0];
    [self.arrayOfSections removeObject:section1];
    [self.arrayOfSections addObject:section1];

    [self.myTableView moveSection:0
                        toSection:2];

}

I will leave it up to you to decide when you would like to invoke this method, as we don’t have a button on our UI at the moment. You can simply create a navigation controller, place a navigation button on it, and then invoke this method.

Once you run the app normally, you will see the sections lined up from 1 to 3, as in Figure 4-12.

A table view with three sections, each containing three cells
Figure 4-12. A table view with three sections, each containing three cells

After you invoke the moveSection1ToSection3 method, you will see that Section 1 gets moved to Section 3, Section 3 moves to Section 2’s previous position, and finally Section 2 moves to Section 1’s previous position (Figure 4-13).

Section 1 is moved to Section 3, and other sections are subsequently moved as well
Figure 4-13. Section 1 is moved to Section 3, and other sections are subsequently moved as well

Moving cells is very similar to moving sections. To move cells, all we have to do is use the moveRowAtIndexPath:toIndexPath: method. Remember that you can move a cell from one section to the same section, or to a new section. Let’s make it easy and move Cell 1 in Section 1 to Cell 2 in the same section and see what happens:

- (void) moveCell1InSection1ToCell2InSection1{

    NSMutableArray *section1 = self.arrayOfSections[0];
    NSString *cell1InSection1 = section1[0];

    [section1 removeObject:cell1InSection1];

    [section1 insertObject:cell1InSection1
                   atIndex:1];

    NSIndexPath *sourceIndexPath = [NSIndexPath indexPathForRow:0
                                                      inSection:0];
    NSIndexPath *destinationIndexPath = [NSIndexPath indexPathForRow:1
                                                           inSection:0];

    [self.myTableView moveRowAtIndexPath:sourceIndexPath
                             toIndexPath:destinationIndexPath];

}

So what is going on in this code? Well, we need to make sure our data source holds the correct data that needs to be displayed in our table view after we have moved the cells around, so we remove Cell 1 in Section 1 first. That moves Cell 2 to Cell 1, and Cell 3 to Cell 2, with a total of 2 cells in the array. Then we will insert Cell 1 into Index 1 (second object) of the array. That will make our array contain Cell 2, Cell 1, and then Cell 3. After that is done, we have actually moved the cells in our table view.

Let’s make this a bit more difficult. How about moving Cell 2 in Section 1 to Cell 1 in Section 2?

- (void) moveCell2InSection1ToCell1InSection2{

    NSMutableArray *section1 = self.arrayOfSections[0];
    NSMutableArray *section2 = self.arrayOfSections[1];

    NSString *cell2InSection1 = section1[1];
    [section1 removeObject:cell2InSection1];

    [section2 insertObject:cell2InSection1
                   atIndex:0];

    NSIndexPath *sourceIndexPath = [NSIndexPath indexPathForRow:1
                                                      inSection:0];
    NSIndexPath *destinationIndexPath = [NSIndexPath indexPathForRow:0
                                                           inSection:1];

    [self.myTableView moveRowAtIndexPath:sourceIndexPath
                             toIndexPath:destinationIndexPath];

}

The results of this transition are shown in Figure 4-14.

Cell 2 in Section 1 is moved to Cell 1 in Section 2
Figure 4-14. Cell 2 in Section 1 is moved to Cell 1 in Section 2

4.8. Deleting Cells and Sections from Table Views

Problem

You want to delete sections and/or cells from table views using animations.

Solution

In order to delete sections from a table view, follow these steps:

  1. First delete the section(s) in your data source, whether you are using a data model like Core Data or a dictionary/array.

  2. Invoke the deleteSections:withRowAnimation: instance method of UITableView on your table view. The first parameter that you need to pass to this method is of type NSIndexSet and this object can be instantiated using the indexSetWithIndex: class method of NSIndexSet class, where the given index is an unsigned integer. Using this approach, you will be able to delete only one section at a time. If you intend to delete more than one section at a time, use the indexSetWithIndexesInRange: class method of NSIndexSet to create the index set using a range and pass that index set to the aforementioned instance method of UITableView.

If you want to delete cells from your table view, follow these steps:

  1. First, delete the cell(s) from your data source. Again, it doesn’t matter if you are using Core Data, a simple dictionary, array, or anything else. The important thing is to delete the objects that represent the table view cells from your data source.

  2. Now, in order to delete the cells that correspond to your data objects, invoke the deleteRowsAtIndexPaths:withRowAnimation: instance method of your table view. The first parameter that you have to pass to this method is an array of type NSArray that must contain objects of type NSIndexPath, with each index path representing one cell in the table view. Each index path has a section and a row, and can be constructed using the indexPathForRow:inSection: class method of NSIndexPath class.

Discussion

In your UI code, sometimes you might need to delete cells and/or sections. For instance, you might have a switch (of type UISwitch; see Recipe 1.2), and when the switch is turned on by the user, you might want to insert a few rows into your table view. After the switch is turned off by the user, you will then want to delete those rows. It’s not always table view cells (rows) that you have to delete. Sometimes you might need to delete a whole section or a few sections simultaneously from your table view. The key for deleting cells and sections from table views is to first delete the data corresponding to those cells/sections from your data source, and then call the appropriate deletion method on the table view. After the deletion method finishes, the table view will refer back to its data source object. If the number of cells/sections in the data source doesn’t match the number of cells/sections in the table view after the deletion is complete, your app will crash. But don’t worry—if you ever do make this mistake, the debug text that gets printed to the console is descriptive enough to point you in the right direction.

Let’s have a look at how we can delete sections from a table view. For this recipe, we will display a table view on a view controller that is displayed inside a navigation controller. Inside the table view, we will display two sections, one for odd numbers and another for even numbers. We will only display 1, 3, 5, and 7 for odd numbers and 0, 2, 4, and 6 for even numbers. For the first exercise, we are going to place a navigation bar button on our navigation bar and make that button responsible for deleting the section with odd numbers in it. Figure 4-15 shows what we want the results to look like.

The user interface to display two sections in a table view and a button that will delete the Odd Numbers section
Figure 4-15. The user interface to display two sections in a table view and a button that will delete the Odd Numbers section

First things first. Let’s define our view controller:

#import "ViewController.h"

static NSString *CellIdentifier = @"NumbersCellIdentifier";

@interface ViewController () <UITableViewDataSource, UITableViewDelegate>
@property (nonatomic, strong) UITableView *tableViewNumbers;
@property (nonatomic, strong) NSMutableDictionary *dictionaryOfNumbers;
@property (nonatomic, strong) UIBarButtonItem *barButtonAction;
@end

The tableViewNumbers property is our table view. The barButtonAction property is the bar button that we’ll display on the navigation bar. Last but not least, the dictionaryOfNumbers property is our data source for the table view. In this dictionary, we will place two values of type NSMutableArray that contain our numbers of type NSNumber. They are mutable arrays, so that, later in this chapter, we will be able to delete them individually from the arrays in the dictionary. We will keep the keys for these arrays in our dictionary as static values in the implementation file of our view controller, so that we can later simply extract them from the dictionary using the static keys (if the keys were not static, finding our arrays in the dictionary would have to be done with string comparison, which is slightly more time-consuming than simply associating the object with a static key that doesn’t change during the lifetime of our view controller). Now let’s define the static string keys for our arrays inside the data source dictionary:

static NSString *SectionOddNumbers = @"Odd Numbers";
static NSString *SectionEvenNumbers = @"Even Numbers";

@implementation ViewController

We now need to populate our data source dictionary with values before we create our table view. Here is the simple getter method that will take care of populating the dictionary for us:

- (NSMutableDictionary *) dictionaryOfNumbers{

    if (_dictionaryOfNumbers == nil){
        NSMutableArray *arrayOfEvenNumbers =
        [[NSMutableArray alloc] initWithArray:@[
                                                @0,
                                                @2,
                                                @4,
                                                @6,
                                                ]];

        NSMutableArray *arrayOfOddNumbers =
        [[NSMutableArray alloc] initWithArray:@[
                                                @1,
                                                @3,
                                                @5,
                                                @7,
                                                ]];

        _dictionaryOfNumbers =
        [[NSMutableDictionary alloc]
         initWithDictionary:@{
                              SectionEvenNumbers : arrayOfEvenNumbers,
                              SectionOddNumbers : arrayOfOddNumbers,
                              }];

    }
    return _dictionaryOfNumbers;
}

So far so good? As you can see, we have two arrays, each of which contains some numbers (one odd and the other even numbers) and we associate them with the SectionEvenNumbers and SectionOddNumbers keys that we declared before in the implementation file of our view controller. Now let’s go ahead and instantiate our table view:

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.barButtonAction =
    [[UIBarButtonItem alloc]
     initWithTitle:@"Delete Odd Numbers"
     style:UIBarButtonItemStylePlain
     target:self
     action:@selector(deleteOddNumbersSection:)];

    [self.navigationItem setRightBarButtonItem:self.barButtonAction
                                      animated:NO];

    self.tableViewNumbers = [[UITableView alloc]
                             initWithFrame:self.view.frame
                             style:UITableViewStyleGrouped];

    [self.tableViewNumbers registerClass:[UITableViewCell class]
                  forCellReuseIdentifier:CellIdentifier];

    self.tableViewNumbers.autoresizingMask =
        UIViewAutoresizingFlexibleWidth |
        UIViewAutoresizingFlexibleHeight;

    self.tableViewNumbers.delegate = self;
    self.tableViewNumbers.dataSource = self;

    [self.view addSubview:self.tableViewNumbers];

}

The next thing we need to do is populate our table view with data inside our data source dictionary:

- (NSInteger) numberOfSectionsInTableView:(UITableView *)tableView{

    return self.dictionaryOfNumbers.allKeys.count;

}

- (NSInteger) tableView:(UITableView *)tableView
  numberOfRowsInSection:(NSInteger)section{

    NSString *sectionNameInDictionary =
        self.dictionaryOfNumbers.allKeys[section];

    NSArray *sectionArray = self.dictionaryOfNumbers[sectionNameInDictionary];
    return sectionArray.count;

}

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

    UITableViewCell *cell = nil;

    cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier
                                           forIndexPath:indexPath];

    NSString *sectionNameInDictionary =
        self.dictionaryOfNumbers.allKeys[indexPath.section];

    NSArray *sectionArray = self.dictionaryOfNumbers[sectionNameInDictionary];

    NSNumber *number = sectionArray[indexPath.row];

    cell.textLabel.text = [NSString stringWithFormat:@"%lu",
                             (unsigned long)[number unsignedIntegerValue]];

    return cell;

}

- (NSString *) tableView:(UITableView *)tableView
 titleForHeaderInSection:(NSInteger)section{

    return self.dictionaryOfNumbers.allKeys[section];

}

Our navigation button is linked to the deleteOddNumbersSection: selector. This is a method we are going to code now. The purpose of this method, as its name implies, is to find the section that corresponds to all odd numbers in our data source and the table view, and remove that section from both of these. Here is how we will do it:

- (void) deleteOddNumbersSection:(id)paramSender{

    /* First remove the section from our data source */
    NSString *key = SectionOddNumbers;
    NSInteger indexForKey = [[self.dictionaryOfNumbers allKeys]
                             indexOfObject:key];

    if (indexForKey == NSNotFound){
        NSLog(@"Could not find the section in the data source.");
        return;
    }
    [self.dictionaryOfNumbers removeObjectForKey:key];

    /* Then delete the section from the table view */
    NSIndexSet *sectionToDelete = [NSIndexSet indexSetWithIndex:indexForKey];
    [self.tableViewNumbers deleteSections:sectionToDelete
                         withRowAnimation:UITableViewRowAnimationAutomatic];

    /* Finally, remove the button from the navigation bar
     as it is not useful any longer */
    [self.navigationItem setRightBarButtonItem:nil animated:YES];

}

Simple enough. Now, when the user presses the navigation bar button, the Odd Numbers section will disappear from the table view. You can note that there is an animation that gets committed on the table view while the section is being deleted. This is because of the UITableViewRowAnimationAutomatic animation type that we are passing to the withRowAnimation: parameter of the deleteSections:withRowAnimation: method of our table view. Now run the app in iOS Simulator and select Debug Toggle Slow Animations. Then attempt to press the navigation bar button and see what happens. You can see the deletion animation in slow motion. It’s neat, isn’t it? After the deletion is completed, our app will look similar to Figure 4-16.

The section containing odd numbers is removed from the table view
Figure 4-16. The section containing odd numbers is removed from the table view

We now know how to delete sections from table views. Let’s move to deleting cells. We are going to change the functionality of our navigation bar button so that when it is pressed, it will delete all cells in all sections of our table view with a numerical value greater than 2. That includes all odd and even numbers greater than 2. So let’s change our navigation bar button item in the viewDidLoad method of our view controller:

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.barButtonAction =
    [[UIBarButtonItem alloc]
     initWithTitle:@"Delete Numbers > 2"
     style:UIBarButtonItemStylePlain
     target:self
     action:@selector(deleteNumbersGreaterThan2:)];

    [self.navigationItem setRightBarButtonItem:self.barButtonAction
                                      animated:NO];

    self.tableViewNumbers = [[UITableView alloc]
                             initWithFrame:self.view.frame
                             style:UITableViewStyleGrouped];

    [self.tableViewNumbers registerClass:[UITableViewCell class]
                  forCellReuseIdentifier:CellIdentifier];

    self.tableViewNumbers.autoresizingMask =
        UIViewAutoresizingFlexibleWidth |
        UIViewAutoresizingFlexibleHeight;

    self.tableViewNumbers.delegate = self;
    self.tableViewNumbers.dataSource = self;

    [self.view addSubview:self.tableViewNumbers];

}

Figure 4-17 shows the results of our app running in iPhone Simulator.

A button that will delete all cells containing a number greater than 2
Figure 4-17. A button that will delete all cells containing a number greater than 2

The navigation bar button is now connected to the deleteNumbersGreaterThan2: selector. This is a method that we have to implement in our view controller, but before jumping into coding it straightaway, let’s first define what this method should do:

  1. Find both arrays of odd and even numbers in our data source and grab the index paths (of type NSIndexPath) of those numbers that are greater than 2. We will use these index paths to later delete the corresponding cells from the table view.

  2. Delete all the numbers greater than 2 from our data source, in both the even and odd number dictionaries.

  3. Delete the relevant cells from the table view. We collected the index paths of these cells in the first step.

  4. Remove the navigation bar button, since it won’t be of any use after the relevant cells have been deleted from the data source and the table view. Alternatively, if you want, you could just disable this button—but I think removing that button provides a better user experience, since a disabled button is really of no use to the user.

- (void) deleteNumbersGreaterThan2:(id)paramSender{

    NSMutableArray *arrayOfIndexPathsToDelete =
        [[NSMutableArray alloc] init];

    NSMutableArray *arrayOfNumberObjectsToDelete =
        [[NSMutableArray alloc] init];

    /* Step 1: gather the objects we have to delete from our data source
     and their index paths */
    __block NSUInteger keyIndex = 0;
    [self.dictionaryOfNumbers enumerateKeysAndObjectsUsingBlock:
     ^(NSString *key, NSMutableArray *object, BOOL *stop) {

         [object enumerateObjectsUsingBlock:
          ^(NSNumber *number, NSUInteger numberIndex, BOOL *stop) {

              if ([number unsignedIntegerValue] > 2){

                  NSIndexPath *indexPath =
                  [NSIndexPath indexPathForRow:numberIndex
                                     inSection:keyIndex];

                  [arrayOfIndexPathsToDelete addObject:indexPath];
                  [arrayOfNumberObjectsToDelete addObject:number];
              }

          }];

         keyIndex++;
     }];

    /* Step 2: delete the objects from the data source */
    if ([arrayOfNumberObjectsToDelete count] > 0){
        NSMutableArray *arrayOfOddNumbers =
            self.dictionaryOfNumbers[SectionOddNumbers];

        NSMutableArray *arrayOfEvenNumbers =
            self.dictionaryOfNumbers[SectionEvenNumbers];

        [arrayOfNumberObjectsToDelete enumerateObjectsUsingBlock:
         ^(NSNumber *numberToDelete, NSUInteger idx, BOOL *stop) {
             if ([arrayOfOddNumbers indexOfObject:numberToDelete]
                    != NSNotFound){
                 [arrayOfOddNumbers removeObject:numberToDelete];
             }
             if ([arrayOfEvenNumbers indexOfObject:numberToDelete]
                    != NSNotFound){
                 [arrayOfEvenNumbers removeObject:numberToDelete];
             }
         }];
    }

    /* Step 3: delete the cells that correspond to the objects */
    [self.tableViewNumbers
     deleteRowsAtIndexPaths:arrayOfIndexPathsToDelete
     withRowAnimation:UITableViewRowAnimationAutomatic];

    [self.navigationItem setRightBarButtonItem:nil animated:YES];

}

After the user presses the button on the navigation bar, all cells containing a number greater than 2 will be deleted from our data source, and the table view and our app will look like Figure 4-18.

We have deleted all cells with a value greater than 2
Figure 4-18. We have deleted all cells with a value greater than 2

See Also

Recipe 1.2

4.9. Utilizing the UITableViewController for Easy Creation of Table Views

Problem

You want to be able to create table views quickly.

Solution

Use the UITableViewController view controller, which by default comes with a table view controller.

Discussion

The iOS SDK contains a really handy class called UITableViewController that comes predefined with a table view instance inside it. In order to take advantage of this class, all you have to really do is create a new class that subclasses the aforementioned class. Here, I will walk you through the steps necessary to create a new Xcode project that utilizes the table view controller:

  1. In Xcode, from the menu bar, choose File New Project...

  2. On the lefthand side of the screen, make sure the iOS category is selected. Then choose the Application subcategory. On the righthand side, choose the Empty Application template and then press the Next button, as shown in Figure 4-19.

Creating a new empty application that will later contain our table view controller
Figure 4-19. Creating a new empty application that will later contain our table view controller
  1. On the next screen, simply choose a name for your project. Also make sure everything except for the Organization Name and the Company Identifier in your dialog is the same as the one that I am demonstrating to you in Figure 4-20. Once you are done, press the Next button.

Configuring our new empty application in Xcode
Figure 4-20. Configuring our new empty application in Xcode
  1. On the next screen, you are given the opportunity to save your application to disk. Simply save the application in a place that makes sense to you and press the Create button.

  2. In Xcode, choose the File New File... menu.

  3. In the dialog, make sure iOS is the main category on the lefthand side and that Cocoa Touch is the subcategory that is selected. Then on the righthand side of the dialog, choose the Objective-C class as shown in Figure 4-21.

Creating a new class for our table view controller
Figure 4-21. Creating a new class for our table view controller
  1. On the next screen, you get to choose the superclass of your new class. This step is very important. Make sure that you set your superclass to UITableViewController. Also make sure the rest of your settings are the same as those that I am demonstrating in Figure 4-22. After you are done, press the Next button.

Setting the superclass of our new object that will become the table view controller
Figure 4-22. Setting the superclass of our new object that will become the table view controller
  1. On the next screen, you get the chance to save your table view controller in your project. Go on, save it as the ViewController class and press the Create button.

  2. In the implementation file of your app delegate, remember to import this view controller’s header file and then create an instance of this class and set it as the root view controller of your application, as shown here:

#import "AppDelegate.h"
#import "ViewController.h"

@implementation AppDelegate

- (BOOL)            application:(UIApplication *)application
  didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{

    ViewController *controller = [[ViewController alloc]
                                  initWithStyle:UITableViewStylePlain];

    self.window = [[UIWindow alloc]
                   initWithFrame:[[UIScreen mainScreen] bounds]];

    self.window.rootViewController = controller;

    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    return YES;
}

Now if you try to compile your project, you will see that the compiler will give you the following warnings:

ViewController.m:47:2: Potentially incomplete method implementation.
ViewController.m:54:2: Incomplete method implementation.

This simply tells you that there are warnings that you have to take care of in the implementation file of your view controller. If you open this file, you will see that Apple has inserted #warning macros in the table view controller class template, which are causing these warnings to be displayed on your screen. One warning is placed inside the numberOfSectionsInTableView: method and the other one is inside the tableView:numberOfRowsInSection: method. The reason we are seeing these warnings is that we have not coded the logic for these methods. The minimum information that the table view controller must have is the number of sections to display, the number of rows to display, and the cell object to be displayed for each row. The reason you are not seeing any warnings for the lack of cell object implementation is that Apple by default provides a dummy implementation of this method that creates empty cells for you.

Note

The table view controller by default is the data source and the delegate of the table view. You do not have to specify a delegate or a data source separately to the table view.

Now let’s go into the implementation of our table view controller and make sure that we have an array of strings (just as an example) that we can feed into our table view:

#import "ViewController.h"

static NSString *CellIdentifier = @"Cell";

@interface ViewController ()
@property (nonatomic, strong) NSArray *allItems;
@end

@implementation ViewController

- (id)initWithStyle:(UITableViewStyle)style
{
    self = [super initWithStyle:style];
    if (self) {
        // Custom initialization
        self.allItems = @[
                       @"Anthony Robbins",
                       @"Steven Paul Jobs",
                       @"Paul Gilbert",
                       @"Yngwie Malmsteen"
                       ];

        [self.tableView registerClass:[UITableViewCell class]
               forCellReuseIdentifier:CellIdentifier];

    }
    return self;
}

- (void) viewDidLoad{
    [super viewDidLoad];
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView
 numberOfRowsInSection:(NSInteger)section{
    return self.allItems.count;
}

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

    UITableViewCell *cell = [tableView
                             dequeueReusableCellWithIdentifier:CellIdentifier
                             forIndexPath:indexPath];

    cell.textLabel.text = self.allItems[indexPath.row];

    return cell;
}

@end

Now if we run our app, we will see something similar to what is shown in Figure 4-23.

Our strings are properly displayed in the table view
Figure 4-23. Our strings are properly displayed in the table view

That’s pretty much all there is to know about table view controllers. Remember, as mentioned before, that your table view controller is the delegate and the data source of your table view now. So you can implement the methods in the UITableViewDataSource protocol as well as the UITableViewDelegate protocol’s methods right in the implementation of your table view controller.

See Also

Recipe 4.1

4.10. Displaying a Refresh Control for Table Views

Problem

You want to display a nice refresh UI control on top of your table views that allows your users to intuitively pull down the table view in order to update its contents. Examples of a refresh control is shown in Figure 4-24.

A refresh control is displayed on top of a table view
Figure 4-24. A refresh control is displayed on top of a table view

Solution

Simply create a table view controller (as discussed in Recipe 4.9) and set its refreshControl property to a new instance of UIRefreshControl class, as shown here:

- (id)initWithStyle:(UITableViewStyle)style{
    self = [super initWithStyle:style];
    if (self) {

        [self.tableView registerClass:[UITableViewCell class]
               forCellReuseIdentifier:CellIdentifier];

        self.allTimes = [NSMutableArray arrayWithObject:[NSDate date]];

        /* Create the refresh control */
        self.refreshControl = [[UIRefreshControl alloc] init];
        self.refreshControl = self.refreshControl;
        [self.refreshControl addTarget:self
                                action:@selector(handleRefresh:)
                      forControlEvents:UIControlEventValueChanged];

    }
    return self;
}

Discussion

Refresh controls are simple visual indicators that appear on top of table views and tell the user that something is about to get updated. For instance, prior to iOS 6, in order to refresh your mailbox in the Mail app, you had to press a refresh button. In the new iOS, now you can simply drag the list of your emails down, as if you wanted to see what’s above there in the list that you haven’t read already. Once iOS detects this gesture of yours, it will trigger a refresh. Isn’t that cool? Twitter’s iPhone app started this whole thing when they added a refresh control to their apps, so kudos to them for this. Apple has realized this is in fact a really nice and intuitive way of updating table views and has since added a dedicated component to the SDK to implement it. The class name for this component is UIRefreshControl.

Create a new instance of this class simply by calling its init method. Once you are done, add this instance to your table view controller, as described in the Solution section of this recipe.

Now you’ll want to know when the user has triggered a refresh on your table view. To do this, simply call the addTarget:action:forControlEvents: instance method of your refresh control and pass the target object and a selector on that object that takes care of the refresh for you. Pass UIControlEventValueChanged to the forControlEvents parameter of this method.

Here—I want to demonstrate this to you. In this example, we will have a table view controller that displays date and time formatted as strings. Once the user refreshes the list by pulling it down, we will add the current date and time again to the list and refresh our table view. This way, every time the user pulls the list down, it triggers a refresh that will allow us to add the current date and time to the list and refresh the table view to display the new date and time. So let’s start in the implementation file of our table view controller and define our refresh control and our data source:

#import "ViewController.h"

static NSString *CellIdentifier = @"Cell";

@interface ViewController ()
@property (nonatomic, strong) NSMutableArray *allTimes;
@property (nonatomic, strong) UIRefreshControl *refreshControl;
@end

@implementation ViewController

The allTimes property is a simple mutable array that will contain all the instances of NSDate in it as the user refreshes the table view. We have already seen the initialization of our table view controller in the Solution section of this recipe, so I won’t write it again here. But as you saw there, we have hooked the UIControlEventValueChanged event of our refresh control to a method called handleRefresh:. In this method, all we are going to do is add the current date and time to our array of date and times and then refresh the table view:

- (void) handleRefresh:(id)paramSender{

    /* Put a bit of delay between when the refresh control is released
     and when we actually do the refreshing to make the UI look a bit
     smoother than just doing the update without the animation */
    int64_t delayInSeconds = 1.0f;
    dispatch_time_t popTime =
        dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);

    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){

        /* Add the current date to the list of dates that we have
         so that when the table view is refreshed, a new item will appear
         on the screen so that the user will see the difference between
         the before and the after of the refresh */
        [self.allTimes addObject:[NSDate date]];

        [self.refreshControl endRefreshing];

        NSIndexPath *indexPathOfNewRow =
            [NSIndexPath indexPathForRow:self.allTimes.count-1 inSection:0];

        [self.tableView
             insertRowsAtIndexPaths:@[indexPathOfNewRow]
             withRowAnimation:UITableViewRowAnimationAutomatic];
    });

}

Last but not least, we will provide the date to our table view through the table view’s delegate and data source methods:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView
 numberOfRowsInSection:(NSInteger)section{
    return self.allTimes.count;
}

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


    UITableViewCell *cell = [tableView
                             dequeueReusableCellWithIdentifier:CellIdentifier
                             forIndexPath:indexPath];

    cell.textLabel.text = [NSString stringWithFormat:@"%@",
                           self.allTimes[indexPath.row]];

    return cell;
}

Give this a go in either the simulator or the device. Once you open the app, at first you will see only one date/time added to the list. Keep dragging the table view down to get more items in the list (see Figure 4-24).

See Also

Recipe 4.9

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required