July 09, 2009

Dashcode: setCurrentViewWithTransition

I haven't offered much discussion of Dashcode or web development in this blog. I still remain convinced that it's a very viable method of iPhone app release, but it's not where my personal attention has been focused these last several months, and thus I've had much more opportunity to write about new SDK code than web extensions.

However, yesterday I got a Dashcode query, and I wanted to share the results.

Dashcode, of course, adds tons of functionality for web app developers, to the point where you can match the look-and-feel of a lot of pure SDK coding. We showed some of this off on on pp.137-138 of iPhone in Action where we explained how to use setCurrentView to allow a simple sliding transition. As is our wont, we also gave you a pointer to a more complex function, setCurrentViewWithTransition, even though our books didn't have space for that additional functionality. Today I'm going to explain that a bit more, since documentation of it online is lacking in any examples.

A setCurrentViewWithTransition Example

setCurrentViewWithTransition allows you to do change your view in Dashcode with much fancier transitions than simple slides. In order to use it, you must first create a "Transition" object, then pass that as an additional (second) argument to the setCurrentViewWithTransition method.

To create a transition, you'll send it three arguments: type, duration, and timing. Both type and timing are constants that can be found here, in the Dashcode documents under the "Stack Layout Transitions" section.

The type constant in particular lets you do a lot of cool stuff, like DISSOLVE, SWAP, and REVOLVE.

Here's an example of actually creating a transition, then applying it to setCurrentViewWithTransition. It's a variant of a function found in Listing 7.3, our Dashcode tab bar.

function gotoPageTwoFor(event)
{
  newTransition = new Transition(Transition.FLIP_TYPE,1,Transition.EASE_TIMING);
  document.getElementById('stackLayout').object.setCurrentViewWithTransition

    ('view2',newTransition,false);
}

That's it, a simple two-step process of creating the transition, then sending it as the second argument of setCurrentViewWithTransition.

Book Update: As of the current version of Dashcode, both setCurrentView and setCurrentViewWithTransition take an additional argument. The correct arg list for setCurrentView (updating what you see on p.137) is "View, Reverse, ToTop" while the correct arg list for setCurrentViewWithTransition is similarly "View, Transition, Reverse, ToTop".

The new "ToTop" variable is a boolean that says whether to scroll the new view to the top. If you omit it (as I did in the above example, to maintain clear consistency with the book example), Dashcode will do the right thing, but it's probably better to start using the updated function call.

June 25, 2009

Table Tricks for iPhone OS 3.0

In March we talked about our intention to start writing articles about the new iPhone OS 3.0 ... and immediately came to a screeching halt as we discovered that Apple had updated their NDA to prevent discussion of prerelease versions of the SDK.

Now, fortunately, iPhone OS 3.0 has been officially released, and so we can start talking about what's changed from when we wrote iPhone in Action. One of the biggest differences shows up in UITableViewCells, with the entire old way to do things being deprecated (though still functional at this time).

Here's what you should know:

Creating Table Cells

We talked about how to fill in a cell for a table on p.235 of iPhone in Action. As of 3.0, however, you no longer work with a bare cell, but instead can use a stylized cell, which will allow you to arrange up to four data elements (two text labels, one image, and one accessory view) in standardized ways. To do so, you create your cell with a new init function:

cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
             reuseIdentifier:CellIdentifier] autorelease];

The style can be set to one of four types:

  1. UITableViewCellStyleDefault creates a old-style cell with just one label.
  2. UITableViewCellStyleValue1 includes two labels, a black one to the left and a blue one to the right, just like the Settings cells that we talked about in a previous article.
  3. UITableViewCellStyleValue2 does the opposite, putting a blue label to the left (and right justifying it) and a black label to the right (and left justifying it), like is shown for the Info page of individual contacts.
  4. UITableViewCellStyleSubtitle puts one line of text in black, then another below it in gray, allowing much the same functionality as shown in our beautiful tables post.

Besides these one or two labels, you can also automatically insert a picture to the left of each table cell and (of course) an accessory to the right. Those are available in all styles.

Accessing the Cell Views

The other big change in cell is how you access its elements. You should no longer be using the simple image and text properties that you depended on in 2.x. Instead, a set of three properties give you access to the actual objects that make up the cell, allowing you to change any of their properties. imageView contains the UIImageView placed to the left, textLabel contains the UILabel for the main (or only) text and detailTextLabel contains the UILabel for the subsidiary text (if there is such). You can manipulate those as you see fit.

Here's an example of some code that fills in both text labels and an image in a subtitle-styled box.

cell.textLabel.text = [textLabelContents objectAtIndex:indexPath.section];
cell.textLabel.font = [UIFont boldSystemFontOfSize:14];

cell.detailTextLabel.text = [detailLabelContents objectAtIndex:indexPath.section]

cell.imageView.image = [imageList objectAtIndex:indexPath.section];

Note that I can change not only the text and image properties of these objects, but also other things, such as the font change shown for textLabel.

This setup has advantages and disadvantages.

On the good side, it's a lot easier to do somewhat more complex things, which should save you a lot of programming time for the standard styles that you develop for filling in table cells.

On the bad side, because so much is done for you, you can end up kind of constrained. For example, I set the textLabel to a font size of 14 in the code above, which is pretty small. The problem is that the whole style is really set up for a larger text size, and so this leaves more space than I'd like between the two lines of text. I suppose I could next go through and move the UILabels around and shrink the rowHeight ... but at a certain point I start to wonder if I should just create my own UITableViewCell subclass from scratch.

Of course, even doing that's probably simpler as long as you start with one of these styles ...

About 3.0 Deployment

When I started thinking about coding with new 3.0 API elements, my first concern was whether I'd be cutting off potential customers by doing so. Fortunately, this doesn't seem to be a concern. The tapbots blog shows a 75% adoption rate of 3.0 over the first 5 days.

That's high enough that I have no qualms about working on new programs using 3.0, though I'd be a bit more careful updating old programs, until that rate gets to 90% or so.

(The biggest problems seems to be Apple's decision to charge iPod users $10 to upgrade their OS.)

June 15, 2009

Animating Sublayers

In iPhone in Action we talked about two ways that you can animate graphics in your apps.

First, we talked about Quartz (pp.367-391). Although we didn't specifically talk about using it for animation, the methodology should be obvious: by redrawing your CALayer in slightly different ways, you can explicitly create an animation.

Second, we talked about Core Animation (pp. 391-394). It makes animation a lot easier, because if you want to animate something in a standard way (such as by adjusting its position or its opacity) you can do so just by changing that value, then having the SDK implicitly create the animation for you.

So, what do you do if you want to create a Quartz drawing (some of which might animate explicitly--or not) that itself contains simple, implicit animations? That's what this article will talk about.

From Quartz to Core Animation

The secret lies in the use of sublayers. We briefly touch upon this on p.392, where we note that you can add sublayers to a layer and then animate the individually. (We also warn about sublayers having inverted coordinate systems, which is no longer the case, as far as I can see.) However, we don't give details because (as ever) there's limited space in a print book.

So, here are the details:

You can easily create a sublayer as part of a drawRect: method call in a UIView subclass. Here's an example of creating a subview that has an arrow drawn on it:

if (!leftLayer) {
    leftLayer = [CALayer layer];   
    leftLayer.frame = CGRectMake(stripX,stripY,arrowSpace,stripHeight);

    UIGraphicsBeginImageContext(CGSizeMake(arrowSpace,stripHeight));
    CGContextRef leftCtx = UIGraphicsGetCurrentContext();

    CGContextSetFillColorWithColor(leftCtx, [toolColor CGColor]);
    CGContextMoveToPoint(leftCtx, arrowSpace, verticalMargin);
    CGContextAddLineToPoint(leftCtx, arrowSpace, stripHeight-verticalMargin);
    CGContextAddLineToPoint(leftCtx, verticalMargin, stripHeight/2);
    CGContextClosePath(leftCtx);
    CGContextFillPath(leftCtx);

    UIImage *leftPic = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    leftLayer.contents = (id)[leftPic CGImage];
    [self.layer addSublayer:leftLayer];
}

There are broadly three steps to creating a sublayer. First, you create the sublayer with the [CALayer layer] message, usually followed by a message to set its frame size. Second, you create the sublayer's content. Third, you add the new layer as a sublayer to your existing CALayer.

Step one requires one additional note. Be aware that we only create this sublayer if it hasn't been created previously. This is a similar methodology to our discussions of creating subviews of a table, several weeks back.

The trickiest element, however, is in step two, because there aren't a lot of easy ways to add content to a sublayer without creating a whole subclass of CALayer, then creating its content there. Though that's surely possibly, it's nice to keep all of your drawing of layers and sublayers together, thus I offer up a pretty good alternate methodology here: create a bitmap content, draw to it, and save it (using the same techniques we discussed on pp.383-384), then set the content property of your sublayer to the CGImage you create from your bitmap.

Animating Your Sublayer

From this point,animating your sublayer should be largely trivial, following our Core Animation example from iPhone in Action. Here's some snippets of code that are used to animate the subview.They appear in touchesMoved:withEvent:

CABasicAnimation *arrowAnim = [CABasicAnimation animationWithKeyPath:@"opacity"];
arrowAnim.duration = .25;
arrowAnim.fromValue = [NSNumber numberWithFloat:1];
arrowAnim.toValue = [NSNumber numberWithFloat:.5];
arrowAnim.autoreverses = YES;
arrowAnim.delegate = self;

CABasicAnimation *arrowMoveAnim = [CABasicAnimation
    animationWithKeyPath:@"transform.translation.x"];
arrowMoveAnim.duration = .25;
arrowMoveAnim.fromValue = [NSNumber numberWithFloat:0];
arrowMoveAnim.toValue = [NSNumber numberWithFloat:-40];
arrowMoveAnim.autoreverses = YES;

[leftLayer addAnimation:arrowAnim forKey:@"animateOpacity"];
[leftLayer addAnimation:arrowMoveAnim forKey:@"animateLayer"];

As you can see, this is very similar to the airplane example we used on p.392, with opacity and movement both being animated. The biggest change is that we work with a property that we don't mention in the book, autoreverses. That tells the animation to go all the way forward, then back out too. In this case it moves an arrow to the left, and then back (which I use in the program to help cue a user that he's moving a strip of icons in that direction).

As always, Core Animation is simple and quick. The only trick in this bit of code for me was figuring out how to create the sublayer and populate it correctly. Beyond that, you existing Core Animation knowledge should work fine.

June 05, 2009

Warning: kCGColorSpaceGenericRGB is deprecated

You might see this warning when you create a color space, for example when you're working toward making a gradient. It's non-fatal, but you should correct it so that your program doesn't fail at some time in the future.

You probably have a line of code something like this:

CGColorSpaceRef myColorSpace =  
    CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);

It should be changed to this:

CGColorSpaceRef myColorSpace = CGColorSpaceCreateDeviceRGB();

You can find out why by reading the CGColorSpace reference, which says:

Prior to Mac OS X v10.4, you could pass this function one of the constants defined in “Named Color Spaces (Deprecated)”. As of Mac OS X v10.4, this function returns a generic color space even if you pass is one of the deprecated named color spaces.

In other words, just use CGColorSpaceCreateDeviceRGB();.

June 03, 2009

UIActionSheet, Part One: Alternate User Input

Your options for how accept input in an iPhone app are limited both by the innate constraints of the screen and by the standard human interfaces which Apple has developed. So, if you want to give users a few options, how do you do it?

Changing State

There are a couple of robust ways to let a user quickly change state:

  • A Table View. iPhone in Action pp.231-238. This is a standard methodology for changing the state of a program. For example, when you click the "More" tab in the "iPod" program you'll see six choices: "Audiobooks", "Compilations", "Composers", "Genres", "Songs", and "Videos". In order to make good use of a table view, you really need to make it beautiful, as discussed in Table Tips & Tricks Part Three. At the least, you should put a graphic to the left, as shown in the "iPod" program.
  • A Tab View Controller. iPhone in Action pp.265-271. This is probably the most common methodology for changing state. In the 'iPod" example given above, the "More" page is actually overflow from a standard tab interface.

I think that using one or both of these methods to allow a user to control state is usually sufficient, particularly if you link them together via a "More" option (which is easy to do with a tab bar).

Selecting Actions

However, as a programmer, I'm a little less happy with the options allowed for selecting actions. Here's a quick run-down:

  • In-Line Buttons. iPhone in Action pp.255-257. In-line buttons aren't used very often in Apple's iPhone Apps, and I think that shows in the very limited functionality that the UIButton currently allows. You basically can't make it look like anything but an ugly white button unless you define a graphic. I've gone the graphic route more than once in programs, but I don't think I've ever shown a bare in-line button in a real program.
  • Navigation Bar Buttons. iPhone in Action pp.275-276. If you have a navigation bar in your program, you can add one button to the right. Further, if you don't mind covering up the back button, you can add one button to the left. This is a pretty good option for user input, but you have to constrain your options to just one or two choices.
  • Tool Bar. iPhone in Action p.287. This expands upon the same idea. You create a toolbar solely to hold a variety of buttons. In iPhone in Action, we make use of it in our photo collage example (pp.351-355), but I'm not convinced of its general utility. I don't find it a particularly attractive interface if you have lots of individual buttons (but, see the Calendar program for a good example of a UIToolBar with a button and a segemented button that ends up looking OK), and it also takes up the space that you'd want to use for a tab bar. Still, it's a possibility if you want more than 1-2 buttons on your page.
  • Touch Control. iPhone in Action pp.240-252. Finally, you can opt to create your own interface by using Quartz to draw an interface and capturing touches to determine what it does. This is a fine option if you want to have some sort of full-screen control, but is a lot of work otherwise.

Putting that all together, it sounds like there's a gap in the support for action user input. If you want to have more than one or two choices, and you want to make it look good, and you want it to be simple ... what do you do?

The answer is that you create an UIActionSheet, one of the SDK classes that we didn't cover in iPhone in Action.

Using a UIActionSheet

Action sheets allow a good alternative for action selection: you don't have to fill up your UI with buttons on every page; instead your main selection window only appears when it's needed. When you request it, it'll display as a menu of buttons which slides up from the bottom of the screen. Consider it the iPhone version of a drag down window.

An action sheet will often be loaded up from a button. Here's an example of some code that can be activated by a button, to give a user two choices of actions:

-(void)popupActionSheet {
    UIActionSheet *popupQuery = [[UIActionSheet alloc]
        initWithTitle:nil
        delegate:self
        cancelButtonTitle:@"Cancel"
        destructiveButtonTitle:nil
        otherButtonTitles:@"Add a Character",@"Delete a Character",nil];

    popupQuery.actionSheetStyle = UIActionSheetStyleBlackOpaque;
    [popupQuery showInView:self.tabBarController.view];
    [popupQuery release];
}

As you can see, creating an action sheet is a three-step process. First, you alloc and init the action sheet, then you set its properties, and finally you send it a showInView: message.

The initWithTitle: function lets you set an optional title and an optional delegate. It also gives you three sorts of buttons that you can create. Destructive buttons have a red background, other buttons have a light gray background, and cancel buttons have a dark gray background.

When the action sheet pops up, it will have indexed all the buttons you supplied in that order: destructive, other, cancel. So, for example, in the above code, my action sheet would appear with these button indices:

0: Add a Character
1: Delete a Character
2: Cancel

If I'd also include a destructive button labeled "Destroy Program", the indices would instead be as follows:

0: Destroy Program
1: Add a Character
2: Delete a Character
3: Cancel

So now you've got a good looking action sheet, how do you respond to it? The answer is, of course, found in that delegate that you set up.

Responding to an Action Sheet

The UIActionSheetDelegateProtocol reference at Apple lists all of your standard delegate methods. The most important one is actionSheet:clickedButtonAtIndex:. This will tell you which of the other or destructive buttons was selected. You can then take actions based on the buttonIndex:

- (void)actionSheet:(UIActionSheet *)actionSheet
    clickedButtonAtIndex:(NSInteger)buttonIndex {

    if (buttonIndex == 0) {
// Add Character
    } else if (buttonIndex == 1) {
// Delete Character
    }
}

That only leaves the cancel button. By default, you can not worry about this, figuring that the system will do the right thing, dismissing the action sheet when you click the button. If you want to do something more, write an actionSheetCancel: method.

And that's it for the basics of action sheets. If there's anything more complex you'd like to know about the class, just let me know.

May 26, 2009

Problems: Can't Build > Clean

This one has bugged me a few times. I go to "Build > Clean" (usually, because my copy of a program on the iPhone isn't updating its pictures) and I find that the options are both grayed out.

The answer to the problem is simple: you're running the program in question either on your simulator or on a connected iPhone. Just close down the program and you should be able to "Build > Clean" immediately thereafter.

May 22, 2009

Creating a Splash Screen: splashView 1.1

In March I created a splashView class which makes it easy to create a splash screen in your iPhone program. Since then, it's achieved a lot of attention,causing me to put together a slightly revised version of the class, along with some hopefully more straightforward instructions on how to use the class.

They follow, below.

The Code

You can download the new splashView class here: Download SplashView 1.1.

I've also put together a sample project with the splashView already in place. You can find that here: Download NavSample. It builds the splashView into the navigation template. That was chosen pretty arbitrarily; you could put the splashView into any other template, as you see fit.

Below is what you need to know about the class. Most of this, I've copied straight from my earlier article, but updated to correctly describe the usage for the current version of splashView. I'll talk about the changes a bit later.

The Methods

There are just two methods intended for external use.

initWithImage:

- (id)initWithImage:(UIImage *)screenImage;

Give the SplashView a 320x460 image that will be used to fill the screen.

startSplash

- (void)startSplash;

Once you've set all your properties, run this to start the timer of the SplashView going.

The Properties

I filled SplashView with properties, so that you can use it in whatever manner you see fit. Here's a run-down of all of the ones intended to be user accessible:

animation (enum). What sort of animation to show when the SplashView disappears. Options are SplashViewAnimationNone, SplashViewAnimationSlideLeft, and SplashViewAnimationFade. Default is SplashViewAnimationNone (no animation).

animationDelay (NSTimeInterval). How long the animation should run. Default is 1s.

delay (NSTimeInterval). How long the SplashView displays before it disappears. Default is 2s.

delegate (id). What object will listen for splashView's optional delegate protocol.

touchAllowed (BOOL). Whether a user can touch the splash screen to immediately make it disappear (or start animating away). Default is NO.

The Protocol

Sometimes you'll want to know when your splashView closes down. That's what the protocol allows. You just need to set your responding object as the splashView's delegate and have it respond to the splashViewDelegate protocol. This will give you access to one protocol method:

splashIsDone

- (void)splashIsDone;

If you want a reminder on how protocols are used, you should consult iPhone in Action p.179. If you want a reminder on how new protocols are created, you should read Create Delegate Protocols for the iPhone, an earlier article.

Using the Class

If you want to use this class in your program, you can do so under a simple Creative Commons license, which basically says that you'll leave our credits in the comments of the class. Other than that, it's free for your use.

Here's the steps to get it running:

  1. Copy the splashView.h and splashView.m files into your program using "Add > Existing Files". When you do so, be sure to check the box that says to physically copy them.
  2. Add the QuartzCore framework to your project.
  3. Decide which file you're going to activate the splashView from. I suggest the app delegate as the most obvious location for this sort of big-picture content, but I think the splashView is now set up correctly so that you can run it from anywhere.
  4. #import "splashView.h" into the appropriate file.
  5. Create your 320x460 PNG. I suggest naming it Default.png. Copy it into your program using "Add > Existing Files", again being sure to check the box so that it gets physically copied.
  6. Code the splashView startup.

Coding the splashView startup just requires the use of the init and startSplash methods previously noted. Here's a minimalist example:

splashView *mySplash = [[splashView alloc] initWithImage:
        [UIImage imageNamed:@"Default.png"]];
[mySplash startSplash];
[mySplash release];

For a less minimalist example, you'd set some of the properties before running startSplash. If anything isn't clear about how to put these pieces together, you should take a look at the navSample class, linked above.

About Default.png:
So why did I suggest using Default.png for the filename? It's because the iPhone will automatically show a file by that name while a program is being loaded. It doesn't do any of the fancy transitions that this class does, nor does it offer any guarantees about how long it'll stay up, but other than that it could be used as a low-budget splash screen (though the purpose was actually to imply that the program was loading faster than it really was).

If you name your image Default.png, then you get the best of both worlds. The PNG will show while the program loads, then the splashView will simultaneously start with your same image, which will eventually transition out via whatever animation method you defined. If you do this, you probably want to tune down the delay once you test it out on a real iPhone, so that users don't feel like they're looking at your splash screen for tool long.

The Changes

I'm going to talk a little bit about the code here, and you might want to first read part two of my original article series, which covers the header file and part three, which covers the source code file. Not only will this give you better context for this update, but it'll also show you the techniques that were used to create the splashView, building on the information from iPhone in Action.

There were two notable changes in this version of splashView.

The first thing I did was improve how the splashView got displayed. In the original version of the program, you had to add the splashView by hand as a subview of the main window. It restricted where you could run splashView, and it also required you to do the right thing or the class wouldn't work.

You'll notice that now you don't add the splashView as a subview at all(!). Somehow, the splashView is magically added to the main window.

It's actually down in the startSplash method, right at the start:

[[[[UIApplication sharedApplication] windows] objectAtIndex:0] addSubview:self];

This takes advantage of the fact that UIApplication has a listing of all the windows, and automatically places splashView atop the first one.

The second thing I did was add the protocol. This methodology is all covered in the creating delegate protocols article I already referenced. After setting up the protocol, I referenced it from dismissSplashFinish:

if (self.delegate != NULL
  && [self.delegate respondsToSelector:@selector(splashIsDone)]) {
    [delegate splashIsDone];
}

As before, if you have questions, comments, or suggestions for improvements, please include them in the comments.

I can't at this time guarantee that the splashView works in iPhone OS 3.0, as I'm currently doing live development on 2.2.1.

May 11, 2009

Table Tricks & Tips, Part Four: Editing Tables

Deltable In some of the iPhone's default apps, you may find that you can "edit" a table, then have little red minus signs appear to the left of the table, giving you the option to delete those rows. So, how do you do that in your own programs?

The answers, it turns out, is a very simple one:

[self.tableView setEditing:YES animated:YES];

That's literally all you need to do to set up those deletion marks. Then you just need to respond to tableView:commitEditingStyle: forRowAtIndexPath:.

However, there are some nuances, particularly the questions of how you start up a table's editing and how you end it, and I'm going to show some real-world examples of those methods over the course of this article ...

An Actual Example

The program that I've showed off elsewhere in this series makes uses of a tableView's editing functionality by allowing the user to delete characters from the app, each of whom are represented by a table row

First, you need to set up some way to activate the functionality:

- (void)editCharacters {
    [self.tableView setEditing:YES animated:YES];
    UIBarButtonItem *doneButton = [[UIBarButtonItem alloc]
        initWithBarButtonSystemItem:UIBarButtonSystemItemDone
                                           target:self
                                           action:@selector(endTableEditing)];
    self.navigationItem.leftBarButtonItem = doneButton;
    [doneButton release];

    self.navigationItem.rightBarButtonItem = nil;
}

This method is triggered when a user selects an "Edit" button. You'll note that besides starting the editing, I also change around the button in my navbar. That's because when a user is editing I no longer need an "Edit" button, but instead require a "Done" button. You might alternatively set buttons' enabled properties to NO ... but in any case, you always need to give users a way to get out of editing mode.

Your tableView: method will probably delete the table item from your table and/or the data store that it originates from:

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

    if (editingStyle == UITableViewCellEditingStyleDelete) {
        [self recordWasDeleted:indexPath.row];
    }
}

For completeness state, I'll also include the method call generated when the "Done" button is clicked:

- (void)endTableEditing {
    [self.tableView setEditing:NO animated:YES];
    [self updateButtons];
}

This is pretty much the opposite of my editCharacters method: the editing is turned back off, then the buttons are returned to their original state.

And that's really all you need to do to edit a table: turn editing on, respond to the deletion message, and turn it back off at the end.

Doing More with Table Edits

You do have some other editing possibilities that I'm not going to cover in depth in this article. Most notably, you can adjust the editingStyle property of any individual cell. Apple claims it's set to UITableViewCellEditingStyleNone by default, but it sure looks to me like it's set to UITableViewCellEditingStyleDelete, which is what shows those handsome red minus marks. If you instead want to insert rows, you can set it to UITableViewCellEditingStyleInsert, and then do the appropriate thing when tableView:commitEditingStyle:forRowAtIndexPath: receives input of type insert.

That's it for me and tables for the nonce. If there's any other topics that you'd like covered regarding them, let me know in the comments. In the meantime, I'm planning to next return to my splashView topic, as some folks have requested in comments.

May 05, 2009

Table Tricks & Tips, Part 3B: ContentView & Cell Images Done Right

The advantage of writing a book is that you can go back and revamp and revise what you've already written as you go forward and learn more. I did that as I wrote iPhone in Action (which is why you'll find many handy forward and backward references throughout the book) and I've done it in most of my other major works.

When you're publishing live on the internet you don't have that same opportunity. (Or, rather you do, but if you take advantage of it by editing old pieces, you might leave people who read those pieces and moved on with the wrong impression.) So in this article I'd like to polish up some of my earlier discussions of contentView.

In Part One of this series, I offered a very simplified look at contentView, but there was the caveat that it wouldn't work if the table was refreshed (because the contentViews would get created again).

In Part Three of this series, I offered a somewhat clumsy way to keep your contentViews clean, by removing old contentViews whenever you redrew the table. It worked, but I'm certain it lacked something in efficiency.

The ContentView Done Right

So, let me offer an adjunct to those two articles by showing a clean and correct way to work with contentView:

#define topTag 1
#define botTag 2

 static NSString *CellIdentifier = @"Cell";

UILabel *topLabel;
UILabel *botLabel;

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

if (cell == nil) {
    cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero
        reuseIdentifier:CellIdentifier] autorelease];

    topLabel = [[UILabel alloc] initWithFrame:CGRectMake(65,0,205,35)];
    topLabel.tag = topTag;
// Other standard setup like fonts and colors
    [cell.contentView addSubview:topLabel];
    [topLabel release];

    botLabel = [[UILabel alloc] initWithFrame:CGRectMake(65,30,205,20)];
    botLabel.tag = botTag;
// Other standard setup like fonts and colors
    [cell.contentView addSubview:botLabel];
    [botLabel release];        
} else {
    topLabel = (UILabel *)[cell viewWithTag:topTag];
    botLabel = (UILabel *)[cell viewWithTag:botTag];   
}

topLabel.text = [NSString stringWithFormat:@"%@",thisName];
botLabel.text = [NSString stringWithFormat:@"%@",thisText];

cell.image = thisImage;

return cell;

Similar to my earlier methodology, this differentiates between a draw and a redraw by seeing if the cell exists. However now, instead of the inefficient destroying and recreating of subviews, this method just reuses the existing subviews by changing their text. You could similarly change images in UIImageViews, which are the other element you're likely to use in a table cell (but more on a better way to implement some of those momentarily).

The trick is finding your old subviews a second time through. This is done by tagging each subview with an integer that describes it, then running the viewWithTag: method on the cell when you return, which will identify the subview which have the tag in question.

Do not ever use or look for a "0" tag as that will identify everything that's untagged.

The Cell Image Done Right

There's one other bit of magic here: the cell's image property. This property will take the image it's passed and place it to the left of the cell. It's the easiest way to drop an image to the left of a table cell, as you often see in Apple's programs. It doesn't save a lot of effort, but it does save you the work of creating an individual subview for the image yourself.

April 29, 2009

Table Tricks & Tips, Part Three: Beautiful Tables

Tabl1 We talk several times in iPhone in Action about the importance of tables as a central programming paradigm on the iPhone. The problem with this methodology is that, by default, a listing of black text on white backgrounds just doesn't look that interesting. As an example, I offer the table shown at right, a part of an app that I've been working on which very simply lists a game character's name and level, while offering a chevron to click through to a page with more information.

This table is simple and utilitarian. It provides the information that's required, but with very little attention to beauty ... and as such it falls down a little when viewed alongside some of the standard iPhone apps, because Apple did spend a bit of time figuring out how to make their tables look nicer, as you can see if you call up some of the Apple programs, such as iPod, Mail, and Photos.

Generally, Apple uses two tricks to make their tables look nicer:

First, they often have a picture off to the left of each table cell, that in some way depicts the individual item described in the cell. In iPod, that's usually an album picture, in Mail that's a simple icon to describe each of your mail boxes, and in Photos that's a thumbnail of the most recent photo in the set.

Second, Apple often provides a variety of information within a table cell. The iPod list of albums shows off a really standard mechanism. It displays two rows of text: the first one in bold, listing the album name, and the second in normal, GrayColor text, showing the artist name. Mail's folder page shows another example: sender name, subject, time, and content are shown in a variety of sizes, weights, and colors.

Returning to contentView

Tabl2 So, how do you make your table views attractively depict a variety of content and/or pictures? The answer is to make use of the table feature that I described in the first article in this series: contentView.

At the time I described contentView mainly as a tool that you could use to mimic a specific sort of Setting. However, you've probably already realized that its utility can go far beyond that. You can also use it to hold together all the building blocks that make up a more complex cell, whether they be images or text.

At right, I've shown how I rebuilt my page out of three building blocks. First up is a button (masquerading as an image) which I place to the left. Second is a large, bold UILabel listing the name of the character. Third is the level, sitting below the character in darkGrayColor text of a smaller size.

Building these cells was a simple process of creating the three object types that I wanted, positioning each within the bounds of the cell, then adding each to the content view in turn.

Following is a somewhat shortened look at how these things were all done:

- (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];
    } else {
        NSArray *cellSubs = cell.contentView.subviews;
        for (int i = 0 ; i < [cellSubs count] ; i++) {
            [[cellSubs objectAtIndex:i] removeFromSuperview];
        }
    }

    UILabel *nameLabel = [[UILabel alloc] initWithFrame:CGRectMake(65,0,205,35)];
    ...
    [cell.contentView addSubview:nameLabel];
    [nameLabel release];

    UILabel *levelLabel = [[UILabel alloc] initWithFrame:CGRectMake(65,30,205,20)];
    ...
    [cell.contentView addSubview:levelLabel];
    [levelLabel release];

    NumberedButton *checkbutton = [NumberedButton
        buttonWithType:UIButtonTypeCustom];
    ...
    [cell.contentView addSubview:checkbutton];

    cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
    return cell;
}

There are two gotchas to contentViews that I didn't cover in my prior article on the topic.

First, when you're adding subviews to your contentView, you have to be careful about not creating a memory leak by recreating the same subviews again and again.

Often you'll only create a subview when you create a cell for the first time (cell == nil). In this case I instead removed the old subviews on subsequent returns to this function (because the cells can be easily moved around, meaning that the new cell content may no longer match the old subview; it'd still probably be better to use the old subviews, and I suspect I'll update that before I send the project off to the app store).

(And indeed I did; you can find my updated code in: Part 3B.)

Second, you need to either build within the constraints of a table cell's standard height (about 30 pixels) or else explicitly change the height. If you're displaying multiple rows of text, as in my example, you'll probably do the latter. The following appears in my table's ViewDidLoad:

self.tableView.rowHeight = 55;

The result of an extra 20 or 30 lines of code and $5 worth of (great) stock art is a table that's vastly more interesting.

Adding Color to the Mix

Tabl3 Apple usually stops right here in their table beautification. However, two of their standard apps show off a bit more ... color.

The Weather app looks quite attractive because of great use of graphics and because it colors tables in alternating shades of gray (though the Weather app might actually mimic a table rather than using one explicitly).

The Notes main page shows a great use of a yellow background and a brown navigation bar which together imply a pad of legal paper.

You can similarly color your tables with two simple variables.

To color your Navigation Controller's Navigation Bar, set its tintColor property.

To color your page backgrounds, set the backgroundColor of your table view controller's view.

When you're working with table cell subviews, you'll need to set the background color of those as well. The easiest solution is probably to set them to the color of their table view or else to a clearColor, so that you only have to adjust the color of your application in one place.

Following is a code snippet from my app delegate, which depicts how I quickly colored all of the navigation bars and all of the views in my application.

for (int i = 0 ; i < 3 ; i++) {

    [[[tabBar.viewControllers objectAtIndex:i] navigationBar]
     setTintColor:[UIColor colorWithRed:.4 green:.2 blue:.6 alpha:1]];

    [[[[tabBar.viewControllers objectAtIndex:i] topViewController] view]
     setBackgroundColor:[UIColor colorWithRed:.8 green:.8 blue:1 alpha:1]];

}

The results are shown above. I invite you to compare them to the table at the top of this article, and decide which one you'd prefer to have in your app.


Some icons by Joseph Wain / glyphish.com
Weapons stock art by Rita (SaDE) / DriveThruRPG.com