A Table View Based App for iPhone (Part 4)

In part 3 [link to Part 3 blog] of this blog, we added functionality to our table view app to add and delete rows from the table and from the data source. The source file for part three is available [link to Friends3 code zip] here, and the full source for this part is available [link to Friends4 code zip] here.

In this final part of this blog, we’ll be exploring how to group the rows of a table view and sort them alphabetically. So let’s get started!

Open the Friends.xcodeproj file from part 3 and navigate to the Friends.h file. We will be adding a method that will return a new dictionary containing values corresponding to the names in the friendsDictionary object, and keys that are the unique first letters of those names. Here is Friends.h:

#import <foundation/Foundation.h>

@interface Friends : NSObject

@property (nonatomic, strong) NSDictionary *friendsDictionary;

- (void) saveFriendsToPlist:(NSString *) filename;
- (void) loadFriendsFromPlist:(NSString *) filename;
- (void) deleteEntryWithKey:(NSString *)key;
- (void) addEntryWithKey:(NSString *)key andValue:(NSArray *)value;
- (NSDictionary *) dictionaryKeyedByFirstCharOfKeys;

@end

In Friends.m, we add the definition of the dictionaryKeyedByFirstCharOfKeys method, and also add a couple of private methods to help obtain this dictionary:

#import "Friends.h"

@interface Friends()

- (NSArray *) sortedArrayOfKeys;
- (NSArray *) arrayOfKeysHavingFirstChar:(NSString *)firstChar;

@end

@implementation Friends

@synthesize friendsDictionary;

- (void) saveFriendsToPlist:(NSString *) filename
{
    NSString *docPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSString *plistPath = [docPath stringByAppendingPathComponent:filename];
    [self.friendsDictionary writeToFile:plistPath atomically:YES];
}

- (void) loadFriendsFromPlist:(NSString *) filename
{
    NSString *docPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSString *plistPath = [docPath stringByAppendingPathComponent:filename];
    self.friendsDictionary = [NSDictionary dictionaryWithContentsOfFile:plistPath];
}

- (void) deleteEntryWithKey:(NSString *)key
{
    NSMutableDictionary *tempFriends = [self.friendsDictionary mutableCopy];
    [tempFriends removeObjectForKey:key];
    self.friendsDictionary = tempFriends;
    tempFriends = nil;
}

- (void) addEntryWithKey:(NSString *)key andValue:(NSArray *)value
{
    NSMutableDictionary *tempFriends = [self.friendsDictionary mutableCopy];
    [tempFriends setObject:value forKey:key];
    self.friendsDictionary = tempFriends;
    tempFriends = nil;
}

- (NSDictionary *) dictionaryKeyedByFirstCharOfKeys
{
    NSMutableDictionary *tmpDictionary = [[NSMutableDictionary alloc] initWithCapacity:5];
    NSArray *sortedKeys = [self sortedArrayOfKeys];
    for (NSString *name in sortedKeys) {
        NSString *firstChar = [name substringToIndex:1];
        [tmpDictionary setObject:[self arrayOfKeysHavingFirstChar:firstChar] forKey:firstChar];
    }

    return [NSDictionary dictionaryWithDictionary:tmpDictionary];
}

#pragma mark - private methods in extension:

- (NSArray *) sortedArrayOfKeys
{
    return [[self.friendsDictionary allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
}

- (NSArray *) arrayOfKeysHavingFirstChar:(NSString *)firstChar
{
    NSMutableArray *tmpReturnArray = [[NSMutableArray alloc] initWithCapacity:5];
    for (NSString *name in [self sortedArrayOfKeys]) {
        if ([[name substringToIndex:1] isEqualToString:firstChar]) {
            [tmpReturnArray addObject:name];
        }
    }
    return [NSArray arrayWithArray:tmpReturnArray];
}

@end

Notice that we have declared the two private methods in a class extension (an @interface at the top of the implementation file). This is not strictly necessary, but is a good habit to get into. The dictionaryKeyedByFirstCharOfKeys method uses both arrayOfKeysHavingFirstChar: and sortedArrayOfKeys to build the new dictionary. If we put a log statement in the view controller that calls this method, we can see a sample of the output it will produce:

The keys of this new dictionary will become the headers for the sections in the table view, and the values will be the keys we use to populate the cells in the table view.

Now we must set up our table view controller to display its data using a grouped (rather than single section) mode. The first step is to instantiate the controller using a grouped style in AppDelegate.m:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    self.friendsViewController = [[FriendsViewController alloc] initWithStyle:UITableViewStyleGrouped];
    self.navController = [[UINavigationController alloc] initWithRootViewController:self.friendsViewController];
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window setRootViewController:self.navController];
    [self.window makeKeyAndVisible];
    return YES;
}

Now make the following changes to FriendsViewController.m (the entire file is listed, as there are several changes to be made):

#import "FriendsViewController.h"

@interface FriendsViewController ()

@end

@implementation FriendsViewController

@synthesize friends;
@synthesize customCell;
@synthesize addingViewController;

- (id)initWithStyle:(UITableViewStyle)style
{
    self = [super initWithStyle:style];
    if (self) {
        // Custom initialization
        [self.friends loadFriendsFromPlist:@"friends.plist"];
        self.addingViewController.delegate = self;
    }
    return self;
}

#pragma mark - Lazy Instantiation:

- (Friends *)friends
{
    if (!friends) {
        friends = [[Friends alloc] init];
    }
    return friends;
}

- (AddingViewController *)addingViewController
{
    if (!addingViewController) {
        addingViewController = [[AddingViewController alloc] initWithNibName:nil bundle:nil];
    }
    return addingViewController;
}

#pragma mark - Adding View Controller push and delegate:

- (void) displayAddingViewController
{
    self.addingViewController.title = @"Add a Friend";
    [self.navigationController pushViewController:self.addingViewController animated:YES];
}

- (void) userDidAddFriend:(NSString *)name email:(NSString *)email phone:(NSString *)phone
{
    NSArray *valueArray = [NSArray arrayWithObjects:email, phone, nil];
    [self.friends addEntryWithKey:name andValue:valueArray];
    [self.friends saveFriendsToPlist:@"friends.plist"];
    [self.tableView reloadData];
    //NSLog(@"%@", [friends dictionaryKeyedByFirstCharOfKeys]);
}

#pragma mark - UIViewController Delegate Methods:

- (void)viewDidLoad
{
    [super viewDidLoad];

    // Uncomment the following line to preserve selection between presentations.
    // self.clearsSelectionOnViewWillAppear = NO;

    // Uncomment the following line to display an Edit button in the navigation bar for this view controller.

    UIBarButtonItem *addButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(displayAddingViewController)];

    self.navigationItem.rightBarButtonItems = [NSArray arrayWithObjects:self.editButtonItem, addButtonItem, nil];

}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    // Return the number of sections.
    return [[[self.friends dictionaryKeyedByFirstCharOfKeys] allKeys] count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    // Return the number of rows in the section.
    NSDictionary *groupDictionary = [self.friends dictionaryKeyedByFirstCharOfKeys];

    NSArray *sortedKeys = [[groupDictionary allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
    return [[groupDictionary valueForKey:[sortedKeys objectAtIndex:section]] count];
}

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
    return [[[[self.friends dictionaryKeyedByFirstCharOfKeys] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)] objectAtIndex:section];
}

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

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"CustomCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        [[NSBundle mainBundle] loadNibNamed:@"CustomCell" owner:self options:nil];
        cell = self.customCell;

        //[cell setBounds:CGRectMake(0, 0, self.tableView.bounds.size.width, 77)];
        self.customCell = nil;
    }

    // Configure the cell...

    NSArray *sectionKeys = [[[self.friends dictionaryKeyedByFirstCharOfKeys] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
    NSString *sectionName = [sectionKeys objectAtIndex:indexPath.section];
    NSArray *friendKeys = [[self.friends dictionaryKeyedByFirstCharOfKeys] objectForKey:sectionName];
    NSString *friendName = [friendKeys objectAtIndex:indexPath.row];
    NSLog(@"%@, %@, %@, %@", sectionKeys, sectionName, friendKeys, friendName);
    UILabel *label;
    label = (UILabel *)[cell viewWithTag:1];
    label.text = friendName;

    label = (UILabel *)[cell viewWithTag:2];
    label.text = [[self.friends.friendsDictionary objectForKey:friendName] objectAtIndex:0];

    label = (UILabel *)[cell viewWithTag:3];
    label.text = [[self.friends.friendsDictionary objectForKey:friendName] objectAtIndex:1];

    return cell;
}

/*
// Override to support conditional editing of the table view.
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Return NO if you do not want the specified item to be editable.
    return YES;
}
*/

// Override to support editing the table view.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        // Delete the row from the data source
        UILabel *nameLabel = (UILabel *)[[tableView cellForRowAtIndexPath:indexPath] viewWithTag:1];
        NSString *name = nameLabel.text;
        [self.friends deleteEntryWithKey:name];
        [self.friends saveFriendsToPlist:@"friends.plist"];
        [self.tableView reloadData];
    }
    else if (editingStyle == UITableViewCellEditingStyleInsert) {
        // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
    }
}

/*
// Override to support rearranging the table view.
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath
{
}
*/

/*
// Override to support conditional rearranging of the table view.
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Return NO if you do not want the item to be re-orderable.
    return YES;
}
*/

#pragma mark - Table view delegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Navigation logic may go here. Create and push another view controller.
    /*
     <#DetailViewController#> *detailViewController = [[<#DetailViewController#> alloc] initWithNibName:@"<#Nib name#>" bundle:nil];
     // ...
     // Pass the selected object to the new view controller.
     [self.navigationController pushViewController:detailViewController animated:YES];
     */
}

@end

We alter the numberOfSectionsInTableView: and numberOfRowsInSection: to take advantage of our new groupable data in the Friends class. Also note that we supply a header for each section in the tableView: titleForHeaderInSection: method by alphabetizing the keys of the dictionary returned by the dictionaryKeyedByFirstCharOfKeys method of the Friends class.

The most important changes are made in the tableView: cellForRowAtIndexPath: method. These changes are necessary to get the proper entry for each section and each row in the section. Study the code carefully to understand how it works.

There are two modes for the display of a grouped table view, controlled by settings in the xib file. By default, when we run the app now, the table view will be displayed like this, with the headers as moving bars above each section:

If we modify the style in the Attributes Inspector for the table view (in FriendsViewController.xib) as shown here, the table view will be grouped with a larger division between each section.

Play around with various settings to see the result, and enjoy developing Table View Applications!

  • Rick
  • December 28, 2012
  • iOS

About the Author

Leave a Reply 0 comments

Leave a Reply: