Introducing Three20

An Objective-C library for iPhone developers.

One of the things I love about the iPhone, is how consistent most apps are with the OS look and feel, while retaining their unique character. However, I didn’t figure out how to replicate some of the fancier features, until I discovered the Three20 library.

Three20 is an open source Objective-C library that was originally developed for the Facebook App, but has since been used by many other well known brands and projects. It provides powerful view controllers, and it is modular so you can choose which elements to include, or create extensions.

For this article, I will walk through the steps required to make a simple RSS reader that implements the cool pull-to-reload feature found in many iPhone applications. We will install the Three20 library, then step through the code examples required to make it happen.

Installation

Installing the Three20 library takes a fair amount of time, but following the instructions is simple. You should have a good place to store projects and experiments, I created a ‘Projects’ folder in my home directory for this purpose, and I’ll assume that you have made a similar decision.

First of all, open up a Terminal window and navigate to your projects directory (cd to change directory, so type cd Projects or your folder name). Enter the following command to download and install Three20′s required libraries. As you can see, we are cloning a git repository.

git clone git://github.com/facebook/three20.git

asn’t that bad was it? It may take several minutes to download all the files, so while that is working, we’ll setup your Xcode project. Launch Xcode and click “Start a new Project”, then select “iOS Project” and finally “View Based Project” before clicking “Next”.

You will need to enter a name. I used “BuildMobileThree20” and it will be easier for you to follow along if you do the same, especially if you are still learning your way around Xcode. With the project established, we need to close Xcode and finish installing the Three20 library.

Return to the Terminal, and check to see that Three20 library has finished downloading. Run the following commands in the Terminal window, which will install the basic Three20 libraries, as well as a very useful XML extension, which will help us read the BuildMobile RSS feed.

python three20/src/scripts/ttmodule.py -p BuildMobileThree20/BuildMobileThree20.Xcodeproj Three20 
python src/scripts/ttmodule.py -p ../BuildMobileThree20/BuildMobileThree20.Xcodeproj/ extThree20XML 

Now you can open up your Xcode project again, and you should see that the Three20 libraries have been included in your build path. With the installation complete, we are all set, and can continue to the stage of building the pieces.

Building the Pieces

Now for the fun stuff, building the little pieces that go together to make our clever little App. One thing I like about libraries, is the gentle way they guide you into following good programming practices, and Three20 is no exception. The pieces we build are nice and neat, regimented individuals.

The first thing we need to make is a Feed Item. A Feed Item is really nothing more than just a collection of variables used to represent a single article, or item in the RSS feed. Create a new file using the Objective-C class with a subclass ‘NSObject’

FeedItem.h

@interface FeedItem : NSObject {
    NSString *title;
    NSString *description;
    NSDate *publishedDate;
    NSString *link;
}

@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *description;
@property (nonatomic, copy) NSString *link;
@property (nonatomic, retain) NSDate *publishedDate;

@end

FeedItem.m

@implementation FeedItem

@synthesize title, description, link, publishedDate;

- (void)dealloc {
    TT_RELEASE_SAFELY(title);
    TT_RELEASE_SAFELY(description);
    TT_RELEASE_SAFELY(link);
    TT_RELEASE_SAFELY(publishedDate);
    [super dealloc];
}

@end

Note the use of TT_RELEASE_SAFELY instead of calling release. TT_RELEASE_SAFELY is a convenience method that will safely release a variable, and then set that variable to nil to prevent you from accidentally calling it again. This is very useful as it may prevent a small mistake from turning into an all night debugging session.

The Model

Now that we have a basic class to hold our ‘FeedItem’ information, we are going to need another object, that will be responsible for downloading the BuildMobile RSS feed and converting it into a collection of ‘FeedItems’. This class, of course, is our ‘FeedModel’.

Thankfully, Three20 has the TTURLRequestModel for this very purpose. To implement a Model, all you need to do is write a method that fires off an asynchronous URL request, using itself as the delegate for the event. By intercepting the requestDidFinishLoad method, we can quickly convert our RSS feed into Feed Items before any of the views relying on the Model are refreshed.

FeedModel.h

#import <Three20/Three20.h>

@interface FeedModel : TTURLRequestModel {
    NSString *rssFeed;
    NSArray *items;
}

@property (nonatomic, retain) NSArray *items;

@end

FeedModel.m

// Send off a request 
- (void)load:(TTURLRequestCachePolicy)cachePolicy more:(BOOL)more {
    if (!self.isLoading) {

        // We create a new Request for the Build Mobile RSS feed
        // requestDidFinishLoad will be called once the request has ended
        TTURLRequest *request = [TTURLRequest requestWithURL:@"http://www.sitepoint.com/feed/" delegate:self];
        request.cachePolicy = cachePolicy;
        request.cacheExpirationAge = TT_DEFAULT_CACHE_EXPIRATION_AGE;

        // This tells the URLRequest to return an XML Document (RSS)
        TTURLXMLResponse* response = [[TTURLXMLResponse alloc] init];
        response.isRssFeed = YES; // RSS needs to be handled differently
        request.response = response;
        TT_RELEASE_SAFELY(response);

        [request send];
    }

- (void)requestDidFinishLoad:(TTURLRequest *)request {
    NSMutableArray *feedItems = [[NSMutableArray alloc] init];

    // Our response should have an XML Object
    TTURLXMLResponse *response = (TTURLXMLResponse*) request.response;
    NSDictionary *feed = response.rootObject;

    NSDictionary *channel = [feed objectForKey:@"channel"];
    NSObject *channelItems = [channel objectForKey:@"item"];
    if ([channelItems isKindOfClass:[NSArray class]]) {
        for (NSDictionary *item in (NSArray*) channelItems) {
            // Get the Required Elements from the Dictionary
            NSDictionary *titleElement = [item objectForKey:@"title"];
            NSDictionary *descriptionElement = [item objectForKey:@"description"];
            NSDictionary *linkElement = [item objectForKey:@"link"];
            NSDictionary *pubDate = [item objectForKey:@"pubDate"];

            // Format the Recieved String into a Date
            NSDateFormatter* dateFormatter = [[NSDateFormatter alloc] init];
            [dateFormatter setTimeStyle:NSDateFormatterFullStyle];
            [dateFormatter setDateFormat:@"EEE, dd MMMM yyyy HH:mm:ss ZZ"];
            NSDate* date = [dateFormatter dateFromString:[pubDate objectForKey:@"___Entity_Value___"]];
            TT_RELEASE_SAFELY(dateFormatter);

            // Build the Feed Item
            FeedItem *feedItem = [[FeedItem alloc] init];
            feedItem.title = [titleElement objectForKey:@"___Entity_Value___"];
            feedItem.description = [descriptionElement objectForKey:@"___Entity_Value___"];
            feedItem.link = [linkElement objectForKey:@"___Entity_Value___"];
            feedItem.publishedDate = date;

            [feedItems addObject:feedItem];
            TT_RELEASE_SAFELY(feedItem);
        }
    }

    self.items = feedItems;
    TT_RELEASE_SAFELY(feedItems);

    [super requestDidFinishLoad:request];   
}

The Data Source

Now we have a ‘FeedModel’ that sits alone and isn’t connected to anything, we need to fix this by building a ‘FeedDataSource’. A Data Source is responsible for holding on to the Model and converting Feed Items returned from the Model into Table Cells required by Three20′s view components.

This separation of content from display logic allows us to easily test the Model for any problems, or quickly swap out one data source for another without changing any complex display logic. This separation of content is the product of the good programming practises Three20 leads us into.

e need to create another new object called ‘FeedDataSource’, as before this is an Objective-C class with a subclass of NSObject. All we need to do is implement a method to return our model, and another method update that Data Sources items once the Table has reloaded our Model.

FeedDataSource.h

#import <Three20/Three20.h>

#import "FeedModel.h"

@interface FeedDataSource : TTListDataSource {
    FeedModel *feedModel;
}

@end

FeedDataSource.m

#import "FeedDataSource.h"
#import "FeedItem.h"

#import <Three20Core/NSDateAdditions.h>
#import <Three20Core/NSStringAdditions.h>

@implementation FeedDataSource

- (id)init {
    if (self = [super init]) {
        feedModel = [[FeedModel alloc] init];
    }
    return self;
}

// Give the model to our TableView to use
- (id<TTModel>)model {
    return feedModel;
}

// Our Model has loaded items, Convert to Table Cells
- (void)tableViewDidLoadModel:(UITableView*)tableView {
    NSMutableArray *items = [[NSMutableArray alloc] init];

    // Convert Feed Items into TableCells
    for (FeedItem *feedItem in feedModel.items) {
        // Get the Heading, make it safe
        NSString *headingString = [feedItem.title stringByRemovingHTMLTags];
        NSString *descriptionString = [feedItem.description stringByRemovingHTMLTags];
        NSString *publishedString = [feedItem.publishedDate formatRelativeTime];

        NSString *content = [NSString stringWithFormat:@"<b>%@</b>n%@n<i>%@</i>",
                             headingString,
                             descriptionString,
                             publishedString];


        TTStyledText* styledText = [TTStyledText textFromXHTML:content lineBreaks:YES URLs:YES];
        [items addObject:[TTTableStyledTextItem itemWithText:styledText URL:feedItem.link]];
    }

    self.items = items;
    TT_RELEASE_SAFELY(items);
}

- (void)dealloc {
    TT_RELEASE_SAFELY(feedModel);
    [super dealloc];
}

@end

The Controller

Now all that’s done we can create a Controller. Essentially we are creating a TableView, so we should extend from TTTableViewController and take advantage of the cool features provided by Three20. The controller needs just a single method to be implemented to create the data source used by the views. Because this is a framework, all classes know how to handle the data.

And now for a little piece of Three20 magic. We go ahead and implement createDelegate by returning a single TTTableViewDragRefreshDelegate. This single line of code adds pull to reload functionality to our table view, how simple is that? And a little bit magic.

@implementation FeedListController

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
        self.title = @"Build Mobile RSS";
        self.variableHeightRows = YES;
    }
    return self;
}

// Returns a FeedDataSource which will create a Model and define our Table View
- (void)createModel {
    self.dataSource = [[[FeedDataSource alloc] init] autorelease];
}

// Adds Pull to Reload to our table, sweet!
- (id<UITableViewDelegate>)createDelegate {
    return [[[TTTableViewDragRefreshDelegate alloc] initWithController:self] autorelease];
}

@end

The App Delegate

So far we have created a ‘FeedItem’ class, the ‘FeedModel’, a ‘FeedDataSource’ and finally a ‘FeedListController’. Now all that is done, we just just need to alter our Application Delegate to use our new controller. This is the final stage, where we pull all of the pieces together.

Th AppDelegate was created when you made the initial project, and will be named BuildMobileThree20AppDelegate if you have followed the instructions so far. We are going to use the TTNavigator to display our controller, and include methods to display any links we click on inside a Web view.

BuildMobileThree20AppDelegate.m

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    
    TTNavigator* navigator = [TTNavigator navigator];
    navigator.persistenceMode = TTNavigatorPersistenceModeTop;
    navigator.window = window;

    TTURLMap *map = navigator.URLMap;
    [map from:@"*" toViewController:[TTWebController class]]; // Shows web addresses in a browser
    [map from:@"tt://buildmobile" toSharedViewController:[FeedListController class]];

    if (![navigator restoreViewControllers]) {
        [navigator openURLAction:[TTURLAction actionWithURLPath:@"tt://buildmobile"]];
    }   

    [self.window makeKeyAndVisible];
    return YES;
}

Click that big ol’ play button, or ‘Run’ in Xcode and you should be reading BuildMobile articles in your very own app in no time. While this is certainly not a comprehensive guide to the Three20 framework, I hope this is enough for you to start experimenting.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • http://alghamdi.us Ans

    Hi,
    I’m getting many errors trying to follow this tut, there are no mentions for any #imports that need to be done. I tried my best but since I’m newbie, I just couldn’t get the The Model to work! All I get is undefined variables.
    Any suggestions?

  • Joe

    I hear this framework gets rejected by Apple often. I also hear that the docs for three20 even mention that it uses APIs that are fragile. Has anyone else heard this. It would suck to use it in an app and have it rejected.

  • http://www.selcukkizilkaya.com Selçuk KIZILKAYA

    it was absolutely best three20 article. Thanks.