10 March 2010

Introduction to Three20

Character Viewer we'll make using Three20

Character Viewer we'll make using Three20

The Three20 Library is a great open source library for the iPhone developed by Joe Hewitt, the developer of the Facebook iPhone app. The library is chock full of useful code for just about any iPhone project. It has an amazing image browser, great asynchronous/web loading support, stylizable views and labels, and a ton more.

In fact, the library is so full of useful stuff that it can be a little difficult getting a handle on it at first. Since I’m just getting started with Three20 myself, I thought I’d write up a tutorial to show a few basic features of Three20 to other newcomers.

In this tutorial, we’ll be making is a viewer for a list of characters you might have in a tabletop RPG. The tutorial will show you how to integrate Three20 into your projects and give you an introduction to two important components in Three20: URL navigation and custom views.

Adding Three20 To Your Project

Create a new project in XCode by going to File\New Project, and select Window-based Application. Click Choose, and name the Project “RPGChars.”

Next you need to get a copy of the three20 library. The best way to get the latest copy of three20 is to use the source control system “git.” If you do not already have this installed, you can download a Mac installer.

Once you have git installed, follow the excellent instructions on the three20 web page for instructions on how to pull down the latest code and add it to your project.

Once you’ve done that, add the following to the top of RPGCharsAppDelegate.h:

#import "Three20/Three20.h"

Compile your project – if it works three20 has been successfully integrated and you can move onto the next step!

Listing Our Characters

The first thing we’re going to do is to add a table view to list our RPG characters.

However before we begin we’re going to need to create some classes to model our characters. Creating a model for the characters has nothing to do with Three20 so isn’t important for the sake of this tutorial, so just download a set of RPG character classes that I made and add them into your project.

Then, right click on Classes and select Add\New File, and select Cocoa Touch Class\Objective-C class\Subclass of NSObject, and name the new file CharacterListController.m (and make sure “Also create CharacterListController.h” is checked), and click “Finish”. Then replace the contents of CharacterListController.h with the following:

#import <Three20/Three20.h>
 
@interface CharacterListController : TTTableViewController {
 
}
 
@end

Here we are creating a subclass of TTTableViewController, Three20′s version of UITableViewController and importing the Three20 header.

Now replace the contents of CharacterListController.m with the following:

#import "CharacterListController.h"
#import "CharacterData.h"
#import "Character.h"
 
@implementation CharacterListController
 
- (id)init {
 
    if (self = [super init]) {
 
        self.tableViewStyle = UITableViewStyleGrouped;
        self.title = @"Characters";
 
        TTListDataSource *dataSource = 
            [[[TTListDataSource alloc] init] autorelease];
 
        NSMutableArray *characters = 
            [[CharacterData sharedCharacterData] characters];
        for(int i = 0; i < [characters count]; i++) {
 
            Character *character = (Character *) [characters objectAtIndex:i];
 
            TTTableItem *tableItem = 
                [TTTableSubtitleItem 
                    itemWithText:character.name
                    subtitle:[NSString stringWithFormat:@"Level %d %@", 
                        character.level, character.rpgClassName]
                    URL:@""];
 
            [dataSource.items addObject:tableItem];
        }
 
        self.dataSource = dataSource;
 
    }
    return self;
 
}

Let’s go through the above code step by step.

The first thing we do is set the style and title of the table view. Then, we construct a class for the data source – in this case a TTListDataSource. There is also a TTSectionedDataSource that lets you group data easily into sections, but we don’t need that in this case – a simple list will do.

We then loop through all of the characters in our model, and one by one we create a table item for them. Three20 comes with many useful table styles to use, and has one that works pretty well for our needs – a TTTableSubtitleItem, so we choose that.

Note that we leave the URL of the table item blank for now – we’ll fill that in later.

Then we set the data source to the class we’ve been constructing, and that’s it! This is literally all of the code we need to display our list of characters in a table view. You can already see how this is much less code than the standard code we’d use to write this.

Now let’s get our app delegate to load up our view controller – by using Three20′s URL navigation systmem.

Three20 and URL Navigation

In a bit we’re going to want to modify our app so that when we tap on a character, it brings up a detail view for that character. Usually to implement this, you’d handle the didSelectRowAtIndexPath method in your table view, and then hard-code the construction of a detail view controller and push it onto the stack of the app’s navigation controller.

Three20 adds a layer of abstraction into the mix by introducing the concept of URL navigation in an iPhone app. Instead of having to hard-code which navigation controller is responsible for the detail view, each cell can have an URL associated with it, and you can register which view controllers can handle which URLs.

The easiest way to explain this is to see it in code. We’re going to set up URL based navigation for all of the view controllers in our app, but to get started we’re going to start with the view controller we just made.

Open up RPGCharsAppDelegate.m and add the following import to the top:

#import "CharacterListController.h"

Then replace the applicationDidFinishLaunching method with the following:

- (void)applicationDidFinishLaunching:(UIApplication *)application {    
 
    TTNavigator *navigator = [TTNavigator navigator];
    navigator.window = window;
 
    TTURLMap *map = navigator.URLMap;
    [map from:@"tt://characterList" 
        toSharedViewController:[CharacterListController class]];
 
    [navigator openURLAction:[TTURLAction actionWithURLPath:@"tt://characterList"]];
 
    // Override point for customization after application launch
    [window makeKeyAndVisible];
}

Here we create a new TTNavigator, which is the object that will handle the loading and displaying of our view controllers. We give it a handle to the main window, and we allow it to create a UINavigationController for us behind the scenes.

We then create a TTURLMap to set up the associations from URL to view controller. We say that whenever someone tries to navigate to the “tt://characterList” URL, that should load up the CharacteListController view controller.

And then we start the app off by launching that URL, which should then load up the CharacterListController – and that’s it!

Compile and run the app, and if all looks well you should see a nice list of characters on the screen:

Screenshot of list of characters

Drilling Down to a Detail View

Now let’s make it so you can tap a character to drill-down into a detail view for that character. We’ll begin by making the detail view itself.

Then, right click on Classes and select Add\New File, and select Cocoa Touch Class\Objective-C class\Subclass of NSObject, and name the new file CharacterDetailController.m (and make sure “Also create CharacterDetailController.h” is checked), and click “Finish”. Then replace the contents of CharacterDetailController.h with the following:

#import <Three20/Three20.h>
 
@interface CharacterDetailController : TTTableViewController {
 
}
 
@end

So far, exactly the same as we did for the CharacterListController – we create a subclass of TTTableViewController. Now replace the contents of CharcterDetailController.m with the following:

#import "CharacterDetailController.h"
#import "CharacterData.h"
#import "Character.h"
 
@implementation CharacterDetailController
 
- (id)initWithCharacterIndex:(int)characterIndex {
 
    if (self = [super init]) {
 
        self.tableViewStyle = UITableViewStyleGrouped;
 
        TTListDataSource *dataSource = 
            [[[TTListDataSource alloc] init] autorelease];
 
        NSMutableArray *characters = 
            [[CharacterData sharedCharacterData] characters];
        Character * character = 
            (Character *) [characters objectAtIndex:characterIndex];
 
        self.title = character.name;
 
        [dataSource.items addObject:[TTTableCaptionItem 
                                     itemWithText:character.name
                                     caption:@"Name"
                                     URL:@""]];        
        [dataSource.items addObject:[TTTableCaptionItem 
                                     itemWithText:
            [NSString stringWithFormat:@"%d", character.level]
                                     caption:@"Level"
                                     URL:@""]];        
        [dataSource.items addObject:[TTTableCaptionItem 
                                     itemWithText:
            [NSString stringWithFormat:@"%d", character.xp]
                                     caption:@"Exp"
                                     URL:@""]];        
        [dataSource.items addObject:[TTTableCaptionItem 
                                     itemWithText:character.rpgClassName
                                     caption:@"Class"
                                     URL:
            [NSString stringWithFormat:@"", characterIndex]]];
 
        self.dataSource = dataSource;
 
    }
    return self;
 
}
 
@end

A few differences this time. First, instead of creating a plain old init method, we have it take an argument that represents the index of the character to load.

Then, we create table items for each part of the character. We use a different Three20 table cell this time – a TTTableCaption item, which sets up a caption on the left and some text on the right, and fill in each item with one part of the character.

To link this view controller in to the rest of the app, we first need to define a URL scheme for this view controller. Go back to RPGCharsAppDelegate.m and add the following import to the top of the file:

#import "CharacterDetailController.h"

Then add the following line right underneath the line where we added the characterList mapping:

[map from:@"tt://character/(initWithCharacterIndex:)" 
    toSharedViewController:[CharacterDetailController class]];

This code says that whenever we come across a URL that begins with “tt://character/”, load up CharacterDetailController and call the initWithCharacterIndex method with the parameter of whatever come after the “tt://character/” portion of the URL.

So for example, if we tried to open the URL “tt:/character/1″, it would call initWithCharacterIndex:1.

Update: Jeff from the comments section has pointed out that when constructing URLs (especially with string parameters), you have to be wary of using slashes in the URL, as Three20 may parse them as parameters. See his comment for more details to avoid this gotcha, or to see how to use a URL to call a function with more than one parameter!

Ok back to code! Now all we have to do is fill in those URL values in CharacterListController.m that we had left blank earlier. Replace the URL:@”" code with the following:

URL:[NSString stringWithFormat:@"tt://character/%d", i]

Compile and run the app, and if all goes well you should be able to dig down into the character details as well!

Screenshot of character details

Where To Go From Here?

Here’s a sample project with all of the code that we’ve developed in the above tutorial. Note that for it to work, the three20 project needs to be a sibling folder to the RPGChars folder, and named “Three20″.

Now that you’ve gotten a taste of Three20, feel free to experiment with some of the other goodies that Three20 has to offer! The best place to start is by loading the TTCatalog sample project that comes with Three20 – it does a great job of demonstrating the
various components that are available to you.

Have you used Three20 in any of your projects, or are you planning to use it? If so, what have you used it for (or what are you going to use it for)?

Category: iPhone

Tags: , , ,

13 Comments

  1. Joe (6 comments) says:

    Hello Ray,

    thanks for yet another wonderful article on iPhone development.

    Cheers,
    Joe

  2. maniacdev (5 comments) says:

    Thanks for the tutorial, that is definitely a useful library. Somehow I’d forgotten about it. Thanks for the reminder!

  3. Jeff (9 comments) says:

    Its worth pointing out, even in a tutorial as basic as this, that if you register a multiple argument method, like:

    [map from:@"tt://character/(initWithCharacterIndex: andDetail:)"
    toSharedViewController:[CharacterDetailController class]];

    that you format up the calling URL separating the values you want to pass with slashes. ie,

    “tt:/character/1/TRUE”

    would call initWithCharacterIndex:1 withDetail:”TRUE”

    It took me forever to work out why my controller wasn’t working – it turned out that I had a single argument of type NSString, but my values contained slashes. ie, imagine I had:

    initWithSkillNamed:(NSString*)name

    and I called one of the skills “Hack/Slash”. The subsequent URL

    tt://skilldisplay/Hack/Slash

    failed to work as expected

  4. Joseph Smith (1 comments) says:

    Thanks for the primer. Very helpful.

  5. Ray Wenderlich (492 comments) says:

    @Jeff – Thanks for pointing this out, seems like a common gotcha! I’ve added a note into the post about your hint. Thanks again!

  6. dave_t (2 comments) says:

    Once again , thanks for a great tutorial!
    much appreciated.

  7. Frank (1 comments) says:

    I am curious on the implemention of using RESTful HTTP request with three20. I am doing some research on how to implement this with three20 using ASIHTTPRequest: http://allseeing-i.com/ASIHTTPRequest.

  8. Joe (6 comments) says:

    How might delegates be set in the ensuing view controllers? For instance, let’s say I want to kick off a Date (Picker) View Controller from one of those table view cells.

    I know I can do this: [map from:@"ms://date" toModalViewController:[DateViewController class]];

    However, I would normally create a VC, set various items (like the delegate) and then push it on to the stack. In this case we have no chance to do that. Or do we?

  9. Ray Wenderlich (492 comments) says:

    @Frank – Sounds like a good idea for a potential future tutorial :]

    @Joe – Good question! I haven’t tried this myself, but did some digging and found this nice Stack Overflow thread that looks like it might be a solution:

    http://stackoverflow.com/questions/2317754/understanding-ttnavigator

  10. Joe (6 comments) says:

    @Ray – Right on! Passing in params via the URL is great when you’ve got alphanumerics … but not so good for objects.

    Fortunately, I see they now have TTURLAction. MUCH better! So the map stays the same. We just call in to it differently.

    In the case of TTTableItem and the like, they take URLs but not URL Actions. For now, I can respond NO to shouldOpenURL: and then use didSelectObject:atIndexPath: to invoke the URL from that object, but now as a URL Action (and with a query to boot).

  11. Tankista (1 comments) says:

    Hi, thanks for the great tutorial.. I wonder if there is a way to use three20 only for certain controllers. Or do I have to use it only in whole application? Thanx

  12. Ray Wenderlich (492 comments) says:

    @Tankista: Yeah you can definitely use it for only certain view controllers, I’ve done that myself in a past app (just wanted to use a few pieces of three20).

Leave a Comment

I'd love to hear your thoughts!

Tip: Want a cool picture next to your comment? Create a Gravatar!