Saving work with Three20! And write almost no code | iosguy.com

An excellent article on Three20, a framework for iOS that is open-source and can really help you with loading data and working with UITableViews. Thank you iosguy.com

 

First thing that comes to mind is a RSSFeedParser (oops, we already have a class name!). Then it would be nice to have an object that represents each feed instead of using dictionaries all over the place, right? We could name this class as FeedItem.

What attributes should our FeedItem own? It would be cool to let the feed reader know the title, date, description, category and maybe an image that is related to that feed? Oh, we also need the link so that the user is able to read the entire feed.

This is a good startup.

Let’s think about the UI. What I came up is pretty much like

It is basic, but also nice, isn’t it?

And what will we present while we are loading feeds? What if some internet connection error happens? What if there is no feed to present at all?

Let’s start presenting very simple screens for these cases (and as homework, you can make them better if you want to).

For the loading screen ..

 

 

For the error screen

 

And finally the No Feed screen:

 

 

Ok, this is pretty much a lot of work for one simple tutorial, isn’t it? Maybe not.

Let’s get started!

….

First of all you need to download the Three20 library (if you did not do it yet) and then create a Simple Application Project for the iPhone.

After that we can go thru the code.

….

Once you created your project, you should be able run it and see a browser presenting the Three20 project page. If you did not see it, you probably didn’t properly rename the Three20 folder from whatever it is named to three20, or you did not create the project at the very same directory that the library is installed.

So, instead of beginning with the sad part I will give to you the source code for the FeedItem and RSSFeedParser classes. Please, download these files and add them to your project as usual.

Now, if you look at your AppDelegate you will see an URL mapping from @”*” toTTWebController.

- (void)applicationDidFinishLaunching:(UIApplication *)application {
    TTNavigator* navigator = [TTNavigator navigator];
    navigator.persistenceMode = TTNavigatorPersistenceModeAll;
    TTURLMap* map = navigator.URLMap;
    [map from:@"*" toViewController:[TTWebController class]];
    if (![navigator restoreViewControllers]) {
         [navigator openURLAction:
            [TTURLAction actionWithURLPath:@"http://three20.info"]];
    }
}

This is the Three20 way to tell what view controller you want to present. Instead of allocating a view controller by yourself every time you want to present it, you just have to ask Three20 to present that URL and it will do the rest for you. The next step is to verify if there is some view controller to restore and if there isn’t, start with what should be our first view controller.

So, let’s add our own URL mapping so that we can open our view controller.

- (void)applicationDidFinishLaunching:(UIApplication *)application {
    TTNavigator* navigator = [TTNavigator navigator];
    navigator.persistenceMode = TTNavigatorPersistenceModeAll;
    TTURLMap* map = navigator.URLMap;
    [map from:@"*" toViewController:[TTWebController class]];
    [map from:@"tt://feed/" toViewController:
           [RSSFeedTableViewController class]];
    if (![navigator restoreViewControllers]) {
        [navigator openURLAction:
           [TTURLAction actionWithURLPath:@"tt://feed"]];
    }
}

XCode may have warned you that RSSFeedTableViewController was not created yet. So let’s add a new Three20 class that inherits from TTTableViewController and name it as RSSFeedTableViewController.

#import "RSSFeedTableViewController.h"
#import "RSSFeedDataSource.h"

@implementation RSSFeedTableViewController

- (id)init {
    if (self = [super init]) {
        self.variableHeightRows = YES;
    }
    return self;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    self.navigationBarTintColor = [UIColor blackColor];
    self.title = @"iOS Guy RSS Feed";
}

#pragma mark --
#pragma mark TTModelViewController methods

- (void)createModel {
    RSSFeedDataSource *feedDataSource =
         [[RSSFeedDataSource alloc] init];
    self.dataSource = feedDataSource;
    TT_RELEASE_SAFELY(feedDataSource);
}

- (id<UITableViewDelegate>)createDelegate {
    return [[[TTTableViewVarHeightDelegate alloc]
                    initWithController:self] autorelease];
}

@end

Every TTTableViewController has a method called createDelegate that returns the delegate for the table view. This method is called by Three20 during it’s lifecycle. On this tutorial we will use the TTTableViewVarHeightDelegate as the delegate for our table view, because it enables the table view cell to tell the delegate what height it needs. This is really very handsome, because it saves some IFs and math.

Beyond the delegate we also need to provide the data source for the table view (just as the SDK approach), and this is done by the createModel method that is also called by Three20.

Now that we specified our dataSource and delegate we don’t have to bother with data handling nor table view cell rending anymore. This is why I took some few seconds to make the navigation bar black and add a title to the screen ;)

What should we do about the dataSource then?

Basically the Three20 concept from DataSource implies that it is responsible for the creation of the DataModel and for it’s handling.

Let’s create a class named RSSFeedDataSource that inherits from TTListDataSource and instantiate our model in it.

#import "RSSFeedDataSource.h"
#import "FeedItem.h"

@implementation RSSFeedDataSource

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

#pragma mark --
#pragma mark Memory Management methods

- (void)dealloc {
    [dataModel release];
    [super dealloc];
}

Now, let’s implement two methods from TTTableViewDataSource:

#pragma mark -
#pragma mark TTTableViewDataSource methods

- (id<TTModel>)model {
    return dataModel;
}

- (void)tableViewDidLoadModel:(UITableView *)tableView {
    NSArray *modelItems = [dataModel modelItems];
    NSMutableArray *updatedItems =
         [NSMutableArray arrayWithCapacity:modelItems.count];
    for (FeedItem *feedItem in modelItems) {
          TTTableMessageItem *item =
             [TTTableMessageItem itemWithTitle:feedItem.title
                                   caption:feedItem.category
                                   text:feedItem.description
                                   timestamp:feedItem.date
                                   imageURL:feedItem.imageURL
                                   URL:feedItem.link];

         [updatedItems addObject:item];
    }
    self.items = updatedItems;
}

The first method tells the DataSource what is it’s model. The second one is called when your model is ready and therefore is a good time to populate our table view with some table items.

Three20 let’s you define table items that are interface representations of your model item. Each table item is mapped to a table view cell that will render that item for you. As we are using a table view cell that comes within Three20, we don’t need to specify this mapping. But as you will need it someday, take a look on

- (Class)tableView:(UITableView*)tableView
               cellClassForObject:(id)object;

method from TTTableViewDataSource.

I chose the TTTableMessageItem as our item because it maps to a cell that already has all the features that we need to present our feeds, just as the very first screenshot.

It is also a task of the data source to handle our data model’s state so that the user can know what is going on. It is time to build those screens for loading, no feeds and internet connection problems. So, let’s add more 4 methods to our data source.

#pragma mark -
#pragma mark Error Handling methods

- (NSString *)titleForLoading:(BOOL)reloading {
    return @"Loading...";
}

- (NSString *)titleForEmpty {
    return @"No feed";
}

- (NSString *)titleForError:(NSError *)error {
    return @"Oops";
}

- (NSString *)subtitleForError:(NSError *)error {
    return @"The requested feed is not available.";
}

Yes, that was very easy. Wasn’t it?

The only missing piece of our application is the data model. This time we have to handle some states, so that we can notify the data source what is going on.

Basically the model fetches data and therefore it will have to connect to the internet, download our RSS and parse it into FeedItems. As all this work is asynchronous and while this all happens the model’s state change from “created” to “loading” and then to “failed” or “completed”.   We have to use some methods from TTModel to handle this state changes.

Our data model interface should look like this:

#import "RSSFeedParser.h"

@interface RSSFeedDataModel : TTModel<RSSFeedParserDelegate> {
     RSSFeedParser *parser;
     BOOL done;
     BOOL loading;
}

- (NSArray *)modelItems;

@end

Then plan is to use those two boolean variables to handle state changes, the parser to parse data from the web site and the modelItems method to retrieve an array of FeedItems when the model is done.

#import "RSSFeedDataModel.h"

@implementation RSSFeedDataModel

- (NSArray *)modelItems {
    return [parser feedItems];
}

#pragma mark --
#pragma mark TTModel methods

- (void)load:(TTURLRequestCachePolicy)cachePolicy more:(BOOL)more {
    done = NO;
    loading = YES;
    parser = [[RSSFeedParser alloc] initWithContentsOfURL:
              [NSURL URLWithString:@"http://iosguy.com/feed/"]];
    parser.delegate = self;
    [parser parse];
}

- (BOOL)isLoaded {
    return done;
}

- (BOOL)isLoading {
    return loading;
}

#pragma mark --
#pragma mark RSSFeedParserDelegate methods

- (void)rssFeedParserDidFinishParsing
               RSSFeedParser *)rssFeedParser {
   done = YES;
   loading = NO;
   [self didFinishLoad];
}

- (void)rssFeedParser:(RSSFeedParser *)rssFeedParser
           didFailWithError:(NSError *)error {
   done = YES;
   loading = NO;

   [self didFailLoadWithError:error];
}

- (void)dealloc {
   [parser release];
   [super dealloc];
}

@end

The code is pretty legible and easy to understand. You should already be able to run the code and see it working.

And if you are the kind of guy that like to see it working before start copying code, you can get the project source code here.

Now take a time to double check the code, debug it and see how it works.

I have two cool surprises for you!

First: Did you try to click on any feeds?

Two: What do you think to be able to pull the table view down to refresh it’s content?

Fortunately we can do this changing just one line of our code. Go back to our view controller and change the createDelegate method to use the TTTableViewDragRefreshDelegate:

- (id<UITableViewDelegate>)createDelegate {
     return [[[TTTableViewDragRefreshDelegate alloc]
         initWithController:self] autorelease];
}

Your app should look like this when you pull the table view down (and when you release it, it will automatically refresh it’s content).

Awesome!

 

Doron KatziPhone Dev