Archive for February, 2009

Customizing the background/border colors of a UITableView (grouped style)

Customizing the background color of a UITableView is somewhat easy, but only if you use the plain style. If you use the grouped style of a table view, it starts to get way more complex. There are no easy ways to quickly set a property for the border color and the background color of the whole table view cell. Well, there are, but if you try to change some of these values and try to use the grouped style, it will look completely wrong.

Below you have a proper grouped table view using the standard Apple theme:

Standard style

Looks pretty good, and it is also very simple to build that in objective-c. Assuming you use Interface Builder for your UI work, just set the table view as a “grouped”, and then set the title property of each cell, like this:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"Cell";
 
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:CellIdentifier] autorelease];
    }
    cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
 
    NSArray *list = [NSArray arrayWithObjects:@"First", @"Second", @"Third", nil];
    cell.text = [list objectAtIndex:indexPath.row];
 
    return cell;
}

So now let’s try something tricky and simply change the backgroundColor property of cell.contentView, as that works fine when dealing with “plain” style table views. The code would be just a bit different:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"Cell";
 
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:CellIdentifier] autorelease];
    }
    cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
 
    NSArray *list = [NSArray arrayWithObjects:@"First", @"Second", @"Third", nil];
    cell.text = [list objectAtIndex:indexPath.row];
 
    cell.contentView.backgroundColor = [UIColor redColor];
 
    return cell;
}

And this is what it looks like:

Broken results

At first I thought about changing cell.backgroundView, but that doesn’t work. So you can see that changing the contentView doesn’t work either, and it actually breaks a bunch of things:

  • No rounded corners on the left side of the table view anymore
  • The background color on the disclosure image is still set to white
  • How do we change the border color anyway?

So after researching this problem for a while, and asking around for a solution, it seemed like there was no way to customize this stuff without a lot of manual work. Indeed that was the case, but Mike Akers (another StackOverflow.com member) posted the source code to a solution that he came up with, and that works very well so far. You can see his source code here for a custom background view with a border.

Here it is a modified version of my code to use his class:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"Cell";
 
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:CellIdentifier] autorelease];
    }
    cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
 
    UILabel *label = [[[UILabel alloc] init] autorelease];
    label.font = [UIFont boldSystemFontOfSize:16.0f];
    label.frame = CGRectMake(40.0f, 10.0f, 220.0f, 22.0f);
    label.textColor = [UIColor whiteColor];
    label.backgroundColor = [UIColor clearColor];
    label.opaque = NO;
 
    NSArray *list = [NSArray arrayWithObjects:@"First", @"Second", @"Third", nil];
    label.text = [list objectAtIndex:indexPath.row];
    [cell.contentView addSubview:label];
 
    CustomCellBackgroundView *bgView = [[CustomCellBackgroundView alloc] initWithFrame:CGRectZero];
    if (indexPath.row == 0) {
        bgView.position = CustomCellBackgroundViewPositionTop;
    } else if (indexPath.row == 1) {
        bgView.position = CustomCellBackgroundViewPositionMiddle;
    } else {
        bgView.position = CustomCellBackgroundViewPositionBottom;
    }
    cell.backgroundView = bgView;
    cell.accessoryView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"more_arrow.png"]];
 
    return cell;
}

And the resulting interface:

Proper background and border colors

Ugly colors, but you get the idea. The code is also way more complex than before, but at least it’s possible to customize the background and border colors of a table view cell. This is more or less what we use on Knee Cap, our iPhone app that handles day to day money loans. This is what our custom table view looks like:

Knee Cap

iPhone SDK: Setting up a SQLite database before first use

It’s quite common to use SQLite databases in iPhone apps to serve as the backend for your product. While there is a way to create the database file dynamically from your objective-c code, it’s way simpler to create it in your Mac development machine, add it to your Xcode project, and then simply write the code to copy the database file from your app bundle to your app’s document directory.

I use the following code in my projects to do just that:

- (NSString *)copyDBToFinalPath {
    NSString *originalDBPath = [[NSBundle mainBundle] pathForResource:@"database_filename_here" ofType:@"db"];
    NSString *path = nil;
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *appSupportDir = [paths objectAtIndex:0];
    NSString *appBundleName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleName"];
    NSString *dbNameDir = [NSString stringWithFormat:@"%@/%@", appSupportDir, appBundleName];
    NSFileManager *fileManager = [NSFileManager defaultManager];
    BOOL isDir = NO;
    BOOL dirExists = [fileManager fileExistsAtPath:dbNameDir isDirectory:&isDir];
    NSString *dbPath = [NSString stringWithFormat:@"%@/database_filename_here.db", dbNameDir];
    if (dirExists && isDir) {
        BOOL dbExists = [fileManager fileExistsAtPath:dbPath];
        if(!dbExists) {
            NSError *error = nil;
            BOOL success = [fileManager copyItemAtPath:originalDBPath toPath:dbPath error:&error];
            if (!success) {
                NSLog(@"error = %@", error);
            } else {
                path = dbPath;
            }
        } else {
            path = dbPath;
        }
    } else if (!dirExists) {
        NSError *error = nil;
        BOOL success =[fileManager createDirectoryAtPath:dbNameDir attributes:nil];
        if (!success) {
            NSLog(@"failed to create dir");
        }
        success = [fileManager copyItemAtPath:originalDBPath toPath:dbPath error:&error];
        if (!success) {
            NSLog(@"error = %@", error);
        } else {
            path = dbPath;
        }
    }
    return path;
}

I use that function like so:

- (void)applicationDidFinishLaunching:(UIApplication *)application {
    NSString *dbPath = [self copyDBToFinalPath];
    self.db = [FMDatabase databaseWithPath:dbPath];
    if (![self.db open]) {
        NSLog(@"Could not open database.");
    }
    //[self.db setTraceExecution:YES];
    //[self.db setLogsErrors:YES];
 
    // ...
    // rest of my code here
    // ...
}

To create that original SQLite database file, just use the standard “sqlite3” command found in Mac OS X like so:

$ sqlite3 database_filename_here.db
SQLite version 3.4.0
Enter ".help" for instructions
sqlite> .read ./schema.sql

The “schema.sql” file is where I store my table definitions, and standard inserts.

iPhone SDK: formatting a numeric value with NSNumberFormatter

While working on an application that downloads a PDF document into a UIWebView, I wrote some code to display a progress bar while the user waits for the file to be downloaded. It turned out to be very simple to do this, but I hit a snag after trying to also display the amount of bytes that has been retrieved so far. Something like “0.45 Mb of 4.56 Mb”.

Problem was that by default NSNumberFormatter displays number with 3 decimal places, and I needed to customize that a bit, so I could get only 2 decimal places, and always a number before the decimal separator. For instance, getting “0.45 Mb” instead of just “.45 Mb”. The magical solution was the setPositiveFormat method, which allows the developer to specify these parameters.

Here’s the code that I ended up using, and it works great:

- (void)createProgressionAlertWithMessage:(NSString *)message {
    progressAlert = [[UIAlertView alloc] initWithTitle:message message:@"Please wait..." delegate:self cancelButtonTitle:nil otherButtonTitles:nil];
    // Create the progress bar and add it to the alert
    progressView = [[UIProgressView alloc] initWithFrame:CGRectMake(30.0f, 80.0f, 225.0f, 90.0f)];
    [progressAlert addSubview:progressView];
    [progressView setProgressViewStyle:UIProgressViewStyleBar];
 
    UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(90.0f, 90.0f, 225.0f, 40.0f)];
    label.backgroundColor = [UIColor clearColor];
    label.textColor = [UIColor whiteColor];
    label.font = [UIFont systemFontOfSize:12.0f];
    label.text = @"";
    label.tag = kDownloadCounterTag;
    [progressAlert addSubview:label];
 
    [progressAlert show];
    [progressAlert release];
}

That’s the method that creates the UIAlertView that will hold the progress bar, and also the UIProgressView itself to display the progress of the download. I added an extra label in there to finally display the actual bytes of the file as it is being streamed over.

For the actual download I’m using NSURLConnection so I can download the file asynchronously, and receive information while the download is progressing.

- (void)viewWillAppear:(BOOL)animated {
    NSString *file = [NSString stringWithFormat:@"http://domain.com/download?id=%@", self.docID];
    NSURL *fileURL = [NSURL URLWithString:file];
 
    NSURLRequest *req = [NSURLRequest requestWithURL:fileURL];
    NSURLConnection *conn = [NSURLConnection connectionWithRequest:req delegate:self];
 
    [self createProgressionAlertWithMessage:@"Downloading document"];
}
 
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    [self.fileData setLength:0];
    self.totalFileSize = [NSNumber numberWithLongLong:[response expectedContentLength]];
}
 
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    [self.fileData appendData:data];
 
    NSNumber *resourceLength = [NSNumber numberWithUnsignedInteger:[self.fileData length]];
    NSNumber *progress = [NSNumber numberWithFloat:([resourceLength floatValue] / [self.totalFileSize floatValue])];
    progressView.progress = [progress floatValue];
 
    const unsigned int bytes = 1024 * 1024;
    UILabel *label = (UILabel *)[progressAlert viewWithTag:kDownloadCounterTag];
    NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
    [formatter setNumberStyle:NSNumberFormatterDecimalStyle];
    [formatter setPositiveFormat:@"##0.00"];
    NSNumber *partial = [NSNumber numberWithFloat:([resourceLength floatValue] / bytes)];
    NSNumber *total = [NSNumber numberWithFloat:([self.totalFileSize floatValue] / bytes)];
    label.text = [NSString stringWithFormat:@"%@ MB of %@ MB", [formatter stringFromNumber:partial], [formatter stringFromNumber:total]];
    [formatter release];
}
 
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    [progressAlert dismissWithClickedButtonIndex:0 animated:YES];
}

I’m hoping this is useful to someone else. Let me know if there’s a better to do some of the things I’m doing here.

iPhone SDK: Customizing back button title

When developing navigation controller-based apps, it’s pretty common to want to customize the title of the back button that is displayed on the navigation bar. Usually the button title is set to the parent view controller’s title, but you can customize that.

All you need to do is add some code to the viewDidLoad method in the parent view controller:

- (void)viewDidLoad {
    [super viewDidLoad];
 
    self.title = @"Title goes here";
 
    // Uncomment the following line to display an Edit button in the navigation bar for this view controller.
    UIBarButtonItem *syncButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh target:self action:@selector(refresh:)];
    self.navigationItem.rightBarButtonItem = syncButton;
    [syncButton release];
 
    UIBarButtonItem *backButton = [[UIBarButtonItem alloc] initWithTitle:@"Back" style:UIBarButtonItemStyleBordered target:nil action:nil];
    self.navigationItem.backBarButtonItem = backButton;
    [backButton release];
 
    [self refreshData];
}

Screenshots of the parent and child view controllers are available below: