Making UITableViews look not so plain

November 19th, 2011 Posted by: - posted under:Featured » Tutorials

As most of you probably know, UITableView’s are incredibly useful and versatile views to be using in your applications.
If you have ever tried to customize a UITableView though, you know that as soon as you start adding lots of UIViews, UILabels, and UImageViews to a cells ContentView, that these tableviews start to scroll slower and slower, and become choppier and choppier.

What we are going to explore today is how to remedy that situation.
To download the entire XCode project, you can find it at: http://github.com/elc/ICB_PrettyTableView

We are going to build a simple contact viewer, that will display the phones contacts. For each contact, if they have a first name, last name, email and phone number, they will be displayed within one cell, with different colors. The reason this is useful is because it provides the basics for customizing UITableViewCells that can really start to make your application look nice, and still scroll well.
If you don’t want to have simulated data inside the simulator, check out this post for copying data from your device to the simulator: How to import contacts into the iphone simulator

In this example, we have a standard UITableViewController. We are going to have a couple class variables defined in the header

#import <UIKit/UIKit.h>
#import <AddressBook/AddressBook.h>
 
@interface ICBTableViewController : UITableViewController
{
    ABAddressBookRef _addressBook;
}
 
@property (nonatomic, retain) NSArray *contacts;
 
@end

ABAddressBookRef _addressBook is defined in our header, so that we don’t have to release it until we dealloc. And the contacts is so that we can hold on to the data for our tableView.
In the main table view controller file we are going to override the – (void)viewDidLoad to provide some initial configuration of the tableView, as well as loading or generating our data. (We will generate fake data for devices or the simulator that don’t have address book data)

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.tableView.backgroundColor = UIColor.blackColor;
    self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
 
    ABAddressBookRef addressBook = ABAddressBookCreate();
    NSArray *tempArray = (NSArray *)ABAddressBookCopyArrayOfAllPeople(addressBook);
    tempArray = [tempArray sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
        NSString *name1 = (NSString *)ABRecordCopyCompositeName((ABRecordRef)obj1);
        NSString *name2 = (NSString *)ABRecordCopyCompositeName((ABRecordRef)obj2);
        return [name1 compare:name2];
    }];
 
    if ([tempArray count] > 0) {
        self.contacts = tempArray;
    } else {
        NSMutableArray *tempMutableArray = [NSMutableArray arrayWithCapacity:100];
        for (int i = 0; i < 100; ++i) {
            NSMutableDictionary *dict = [NSMutableDictionary dictionary];
            if ((i % 9) != 0) {
                [dict setObject:[NSString stringWithFormat:@"FirstName%d", i] forKey:@"firstName"];
            }
            if ((i % 3) == 0) {
                [dict setObject:[NSString stringWithFormat:@"LastName%d", i] forKey:@"lastName"];
            }
            if ((i % 3) == 0 && (i % 2) == 0) {
                [dict setObject:[NSString stringWithFormat:@"emailTest%d@test%d.com", i, i] forKey:@"email"];
            }
            if ((i % 7) == 0) {
                NSString *string = [NSString stringWithFormat:@"%d", i];
                while ([string length] < 10) {
                    string = [string stringByAppendingFormat:@"%@", string];
                }
                [dict setObject:string forKey:@"phone"];
            }
            [tempMutableArray addObject:dict];
        }
        self.contacts = tempMutableArray;
    }
}

As you can see here we have a couple of self.tableView methods we have called to setup the background color, and also the cell separator style.
If you are running this application on a device, or simulator that has contacts, this method will also make a copy of the address book as the data to display. If there is no data in the address book, we create some fake test data just for displaying.
Also don’t forget to include our – (void)dealloc method for releasing our _addressBook variable.

- (void)dealloc
{
    CFRelease(_addressBook);
    [_contacts release];
    _contacts = nil;
}

We have to supply the tableView with our number of rows

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

The next portion we have to override is the – (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath so that we can supply the tableView with our cells.

// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{   
 
    static NSString *CellIdentifier = @"ICBTableViewCellIdentifier";
 
    ICBTableViewCell *cell = (ICBTableViewCell*)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[ICBTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
        cell.textLabel.textColor = UIColor.whiteColor;
    }
    cell.tag = indexPath.row;
 
    NSObject *object = [self.contacts objectAtIndex:indexPath.row];
    if ([object isKindOfClass:NSDictionary.class]) {
        [cell setDictionary:(NSDictionary *)object];
    } else {
        [cell setRecord:(ABRecordRef)object];
    }
 
    return cell;
}

We are checking each object coming out of our array so that we can determine if we need to call the setDictionary, or setRecord method calls.

Now the meat of this tutorial, extending a UITableViewCell.

In our header we are going to define a bunch of strings that we want to display

#import <UIKit/UIKit.h>
#import <AddressBook/AddressBook.h>
 
@interface ICBTableViewCell : UITableViewCell
 
@property (nonatomic, retain) NSString *firstName;
@property (nonatomic, retain) NSString *lastName;
@property (nonatomic, retain) NSString *email;
@property (nonatomic, retain) NSString *phone;
@property (nonatomic, retain) NSString *address;
 
- (void)setRecord:(ABRecordRef)record;
- (void)setDictionary:(NSDictionary *)dict;
 
@end

And then our two set methods:

- (void)setRecord:(ABRecordRef)record
{
    self.firstName = [(NSString *)ABRecordCopyValue(record, kABPersonFirstNameProperty) autorelease];
    self.lastName = [(NSString *)ABRecordCopyValue(record, kABPersonLastNameProperty) autorelease];
    self.email = [self getFirstEmail:record];
    self.phone = [self getFirstPhone:record];
    [self setNeedsDisplay];
}
 
- (void)setDictionary:(NSDictionary *)dict
{
    self.firstName = [dict objectForKey:@"firstName"];
    self.lastName = [dict objectForKey:@"lastName"];
    self.email = [dict objectForKey:@"email"];
    self.phone = [dict objectForKey:@"phone"];
    [self setNeedsDisplay];
}

Some tutorials will have you put this next part into a separate UIView subclass, and add that class as the contentView of this UITableViewCell, but I prefer to override the drawRect of the UITableViewCell, and do all my drawing there.

The first thing I am doing is getting the current graphics context so that we can draw to the screen, clipping to the rect that is passed in drawRect:(CGRect)rect, and then depending on whether this cell is even, I am filling the entire rect with an almost black color, or slightly lighter.

- (void)drawRect:(CGRect)rect
{
    CGContextRef ctx = UIGraphicsGetCurrentContext();
 
    CGContextClipToRect(ctx, rect);
    //If even
    if (((self.tag % 2) == 0)) {
        CGContextSetFillColorWithColor(ctx, [UIColor colorWithWhite:0.1f alpha:1.f].CGColor);
    } else {
        CGContextSetFillColorWithColor(ctx, [UIColor colorWithWhite:0.15f alpha:1.f].CGColor);
    }
 
    CGContextFillRect(ctx, rect);

The next thing I am going to figure out is whether I want this text to be centered in the cell, and I am determining this based off whether there is an email field or not.

    //Vertically center our text, if no email
    BOOL isCentered = (self.email == nil);

And now for the meat of the drawRect method. We calculate the size of firstName, draw it offset from the left by 5, and then draw lastName right after it. We also change the color we are drawing between those two.

    CGRect tempRect;
    CGFloat midY = CGRectGetMidY(rect);
    [[UIColor whiteColor] set];
    UIFont *defaultFont = [UIFont systemFontOfSize:16];
    CGSize size = [self.firstName sizeWithFont:defaultFont];
    if (isCentered == NO) {
        tempRect = CGRectMake(5, 0, size.width, size.height);
    } else {
        tempRect = CGRectMake(5, midY - size.height/2, size.width, size.height);
    }
    [self.firstName drawInRect:tempRect withFont:defaultFont];
 
    [[UIColor lightGrayColor] set];
    size = [self.lastName sizeWithFont:defaultFont];
    if (isCentered == NO) {
        tempRect = CGRectMake(CGRectGetMaxX(tempRect)+5, 0, size.width, size.height);
    } else {
        tempRect = CGRectMake(CGRectGetMaxX(tempRect)+5, midY - size.height/2, size.width, size.height);
    }
    [self.lastName drawInRect:tempRect withFont:defaultFont];

Next we find out if phone actually exists, and if so set the color to red, and draw it to the right of lastName. We also have to make sure we aren’t drawing this outside our boundaries, so we check to see where the end is, and if it is outside, we crop it to 5 pixels from the end.

    if (self.phone != nil) {
        [[UIColor redColor] set];
        size = [self.phone sizeWithFont:defaultFont];
        CGFloat end = CGRectGetMaxX(tempRect) + size.width;
        if (end > rect.size.width) {
            size.width = CGRectGetMaxX(rect) - CGRectGetMaxX(tempRect) - 10; //-10 so that we get 5 from the end of last name, and 5 from the end of rect
        }
        if (isCentered == NO) {
            tempRect = CGRectMake(CGRectGetMaxX(rect) - size.width - 5, 0, size.width, size.height);
        } else {
            tempRect = CGRectMake(CGRectGetMaxX(rect) - size.width - 5, midY - size.height/2, size.width, size.height);
        }
        [self.phone drawInRect:tempRect withFont:defaultFont lineBreakMode:UILineBreakModeTailTruncation];
    }

And finally if our email actually exists draw it on the bottom left.

    if (self.email != nil) {
        [[UIColor blueColor] set];
        size = [self.email sizeWithFont:defaultFont];
        tempRect = CGRectMake(5, midY, size.width, size.height);
        [self.email drawInRect:tempRect withFont:defaultFont];
    }

I hope this helps you in configuring UITableViewCells for your own project, and hopefully will let you start to think about the possibilities.

To download the entire XCode project, you can find it at: http://github.com/elc/ICB_PrettyTableView