10 March 2010

Introduction to Three20

If you're new here, you may want to subscribe to my RSS feed or follow me on Twitter. Thanks for visiting!

 

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


Category: iPhone

Tags: , , ,

29 Comments

  1. Joe (7 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 (15 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 (874 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 (7 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 (874 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 (7 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 (874 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).

  13. WR (3 comments) says:

    Hi Ray,

    I think I’m following the tutorial closely (famous last words) but my CharacterListController isn’t being instantiated. The breakpoint I set in CharacterListController never get hit.

    Any pointers on debugging, or what I may have overlooked?

    I’m using iOS SDK 4.0 and latest (8/19/2010) of the main facebook/three20 repo.

    Any help appreciated. Thanks!

  14. Ray Wenderlich (874 comments) says:

    @WR: I’d double check you have the exact code from the tutorial in applicationDidFinishLaunching. If you do and it still doesn’t work, try comparing your code to the sample project. Let me know how it goes!

  15. WR (3 comments) says:

    @Ray, thanks for the quick response!

    After an hour or so of figuring out how to get the sample project to build with iOS SDK 4.0, I now have it running in the Simulator.

    But now it just sits at the “Loading…” screen and still doesn’t ever hit my breakpoint in CharacterListController’s init method.

    I’m curious to know whether anyone else has sucessfully worked through this tutorial or run the sample project on iOS SDK 4.0 … I could use some guidance. :-)

    I have three20 installed in ‘/’, so I extracted the RPGChars.zip there as well.

    To get the sample project compiling for the simulator I had to:
    1. Modify the Base SDK for the sample project from 3.1.3 to 4.0 (had to do something similar for all the Three20 projects last week)
    2, Add “../three20/Build/Products/three20″ to the Header Search Paths
    3. Update path for the Three20.xcodeproj to be “/three20/src/Three20/Three20.xcodeproj”
    4. Add all (most?) of the other *.xcodeproj files for the other Three20 subprojects
    5. Make sure all the Three20 .a files have their box checked under the little target icon in the XCode file Details view.
    6. Add the Three20 projects as Direct Dependencies in the Project Settings/General screen (optional, but I wanted to re-build everything).

    I still can’t get the sample to build for the actual device, though. In the build results there are 2 linker errors saying that TTEntityTables.o can’t find symbols for ___restore_vfp_d8_d15_regs or ___save_vfp_d8_d15_regs. I tried setting the deployment target back to 3.1.3 but that doesn’t seem to make any difference.

    I did not change any of the code in the sample project (or Three20), just the project settings. And the TTCatalog sample project still works fine on both the simulator and the device.

  16. WR (3 comments) says:

    @Ray: I figured out the build problem for the device config: some of the build settings for the target were still the old settings (for the device config only, though). I deleted them so the target will use the default project settings and now the RPGChars sample project compiles with iOS SDK 4.0 for both the simulator and the device.

    Still no clue why the CharacterListController isn’t being instantiated, though.

  17. EPM13 (1 comments) says:

    I had the “loading” screen not leaving as well with the newer three20 versions. Went back to three20-v2.0-130 and it’s fine.

  18. Ray Wenderlich (874 comments) says:

    @WR: Yeah I ran into that problem before myself, and it was also due to me missing changing the Base SDK to 4.0 in one of the Three20 subprojects. Did you try EPM13′s suggestion of going back to an older Three20 version to fix the character list not being instantiated?

    @EPM13: Good to know that it’s something about the newer three20 versions.

  19. Tony Spinelli (1 comments) says:

    I apologize in advance if my answer does not make sense-I have been doing iPhone development for about 1 week :-)

    @WR: I ran into the same issue and resolved it after reviewing http://three20.info/ui/navigation. The selector must be added to the URL.

    Change:
    [map from:@"tt://characterList"
    toSharedViewController: [CharacterListController class]];
    [navigator openURLAction:[TTURLAction actionWithURLPath:@"tt://characterList"]];

    To:
    [map from:@"tt://characterList/(init:)"
    toSharedViewController:[CharacterListController class]];
    [navigator openURLAction:[TTURLAction actionWithURLPath:@"tt://characterList/(init:)"]];

  20. Vipin (1 comments) says:

    Hi ray,
    Excellent tutorial !

    I have implemented the three20 photo gallery to my project. Can you tell me how to add a button with custom image to the Character List title bar ? This is to go back to the main view controller of my app from which the gallery is called.

    regards.

  21. Ray Wenderlich (874 comments) says:

    @Tony: Awesome, thanks for sharing the solution to that!

    @Vipin: I haven’t played around with it myself, but you should be able to set the buttons on the navigation bar just like you normally would, such as self.navigationItem.leftBarButtonItem = xxx.

  22. j (2 comments) says:

    Thanks for tutorial- is it possible to download your project?

    Mine doesnt ever get passed “Loading…” so I must have failed to follow one of your steps.

  23. jscore (7 comments) says:

    Sorry to resurrect an old post, but had a quick question.

    I see you’re making an init() method that initializes the photos, etc. Where is it configured that this method will be called? Or is that default framework behavior?

  24. Tony (7 comments) says:

    @jscore – see my message from 9/16. It was the default behavior, but in the more recent versions you must explicitly define the method to call.

  25. jscore (7 comments) says:

    @tony

    Thanks. It’s confusing as to what methods get called or not (like why isn’t createModel() being used like in another examples).

    So I should put my code in init(), then return back self. What about viewDidLoad(), anything should be there?

  26. Tony (7 comments) says:

    @jscore-It depends on what you’re doing. What exactly are you trying to do?

    I was creating a program to browse some photo libraries so I assigned a datasource in my init function and then pushed the viewcontroller.

    I had no need for the viewDidLoad method, but it does get called as you would expect.

  27. Ray Wenderlich (874 comments) says:

    @Tony: Thanks again so much for helping out here! :]

  28. mobi (1 comments) says:

    Thank you Ray for sharing your knowledge.

    I am very new to this field..I am trying to get some idea on three20..

    I followed the Instruction given in Three20 website -http://www.three20.info/article/2010-10-06-Adding-Three20-To-Your-Project

    Everything went well but when tried with -c Debug -c Release it didn’t do any modification to project config setting…I did manually the required setting…..

    Then i struck with never ending @loading screen..

    @Tony you saved my day ..Thank you very much for sharing your solution..

    There is a lot of confusion about config settings when using third party modules like three20… an article/blog on config settings will greatly help new commers like me .

    Thanks again to all sharing your experience and knowledge.

I'd love to hear your thoughts!