Three20 Tutorial for iOS

Ray Wenderlich
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 Three20 tutorial to show a few basic features of Three20 to other newcomers.

In this Three20 tutorial, we’ll be making is a viewer for a list of characters you might have in a tabletop RPG. The Three20 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 Three20 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)initWithNavigatorURL:(NSURL*)URL query:(NSDictionary*)query {
 
    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.

Note that the initializer is called initWithNavigatorURL:query rather than plain old init. This is because initWithNavigatorURL:query is the default initializer sent to a view controller, if you don’t override it otherwise. If you make a mistake and use plain old init, you’ll see a “loading” screen since your initializer will never get called.

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)?

Ray Wenderlich

Ray is an indie software developer currently focusing on iPhone and iPad development, and the administrator of this site. He’s the founder of a small iPhone development studio called Razeware, and is passionate both about making apps and teaching others the techniques to make them.

When Ray’s not programming, he’s probably playing video games, role playing games, or board games.

User Comments

33 Comments

[ 1 , 2 , 3 ]
  • can I pass parameters via URL in TTTableTextItem ? for example, in the following codes, I want to achieve that, when clicking the 3 rows named: str1, str2,str3, they all open the same MapViewController from the nib, but want to pass three different parameters to MapViewcontroller separately. thanks

    self.dataSource = [TTListDataSource dataSourceWithObjects:
    [TTTableTextItem itemWithText:str1 URL:@"tt://nib/MapViewController"],
    [TTTableTextItem itemWithText:str2 URL:@"tt://nib/MapViewController"],
    [TTTableTextItem itemWithText:str3 URL:@"tt://nib/MapViewController"],
    nil];
    sunlite
  • Here is a great video that walks you through getting Three20 to run in Xcode 4. I found the explanation from github a little confusing... So I hope this helps anyone who is having trouble.
    http://www.youtube.com/watch?v=huNoG3qM0cM
    Dahs81
  • If u have error compiling in xcode 4.2

    //compile errors:-
    navigator.window = window;
    [window makeKeyAndVisible];

    //fix by doing this:-
    navigator.window = self.window;
    [self.window makeKeyAndVisible];
    firestorm
[ 1 , 2 , 3 ]

Other Items of Interest

Ray's Monthly Newsletter

Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Hang Out With Us!

Every month, we have a free live Tech Talk - come hang out with us!


Coming up in July: Facebook Pop Tech Talk!

Sign Up - July

RWDevCon Conference?

We are considering having an official raywenderlich.com conference called RWDevCon in DC in early 2015.

The conference would be focused on high quality Swift/iOS 8 technical content, and connecting as a community.

Would this be something you'd be interested in?

    Loading ... Loading ...

Our Books

Our Team

Tutorial Team

  • Tammy Coron
  • Tim Mitra

... 49 total!

Update Team

  • Andy Pereira

Editorial Team

... 23 total!

Code Team

  • Orta Therox

... 1 total!

Translation Team

... 33 total!

Subject Matter Experts

  • Richard Casey

... 4 total!