Mega-Gold Challenge Solution: Another Web Service


#1

The toughest part of this challenge was getting the correct access path to properly parse the different JSON objects. What worked for me was to create an array of dictionaries consisting of the US and Netherlands. Then I enumerated through each dictionary in the array using the enumerateKeysAndObjectsUsingBlock: method to set my properties from the different key/value pairs. I then added each object to either the itemsUS or itemsNetherlands mutable array based on the region. Then, I sorted these two arrays by date using an NSSortDescriptor. This enabled the master tableview to display the schedule in proper order:

[code]- (void)readFromJSONDictionary:(NSDictionary *)d
{
NSDictionary *dUS = [d objectForKey:@“United States”];
NSDictionary *dNetherlands = [d objectForKey:@“Netherlands”];
NSArray *array = [NSArray arrayWithObjects:dUS, dNetherlands, nil];

for (NSDictionary *dict in array) {
    [dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        ScheduleItem *i = [[ScheduleItem alloc] init];
        [i setTitle:[obj objectForKey:@"title"]];
        [i setDateBegin:[obj objectForKey:@"class_begins"]];
        [i setDateEnd:[obj objectForKey:@"class_ends"]];
        [i setPrice:[obj objectForKey:@"price"]];
        [i setInstructorOne:[obj objectForKey:@"instructor_one"]];
        [i setInstructorTwo:[obj objectForKey:@"instructor_two"]];
        [i setLocality:[obj objectForKey:@"locality"]];
        [i setSpacesAvailable:[obj objectForKey:@"total_spaces"]];
        [i setRegion:[obj objectForKey:@"region"]];
        
        if ([[i region] isEqualToString:@"United States"])
            [itemsUS addObject:i];
        else 
            [itemsNetherlands addObject:i];
    }];
}
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"dateBegin" 
                                                               ascending:YES];
[itemsUS sortUsingDescriptors:[NSArray arrayWithObject:sortDescriptor]];
[itemsNetherlands sortUsingDescriptors:[NSArray arrayWithObject:sortDescriptor]];

}[/code]
I also had a tough time getting the date to format properly without getting back a null value. I ended up creating a method to convert the original date from the parsed JSON data. I first used NSDateFormatter’s dateFromString: method to create an NSDate object using the format of the raw date. Then I changed to a more readable date style and returned a formatted string using NSDateFormatter’s stringFromDate: method. I used this method when I set the text of each cell’s date property. Anyone know of a quicker way to format a date than this?:

[code] - (NSString *)formatDate:(NSString *)dateJSON
{
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@“yyyy-MM-dd HH:mm:ss Z”];
NSDate *dateObject = [formatter dateFromString];

[formatter setDateStyle:NSDateFormatterMediumStyle];
[formatter setTimeStyle:NSDateFormatterNoStyle];
NSString *formattedDateString = [formatter stringFromDate:dateObject];

return formattedDateString;

}[/code]
This was a difficult challenge but a good learning experience. I created my views programmatically in the AppDelegate and re-used the BNRConnection actor class, the BNRFeedStore class, and the JSONSerializable protocol from the Nerdfeed project. These were all very helpful in keeping the code clutter out of the tableview classes.


#2

I ended up with a simple object model composed of a root object containing an array of region objects sorted by region name (currently two, one for Europe and one for United States, but they may be any number), each in turn containing a name and an array of course objects sorted by begin date, each of them populated with course details (title, location, dates and so on). The job of reading JSON data is distributed between the three classes – all of them are JSONSerializable.
In the master table view controller, the number of sections in the table view is given by the number of region objects (the section titles are obviously the names of the regions), while the number of rows in each section is the number of course objects in each region object. In tableView:cellForRowAtIndexPath: the right course object for a cell can be obtained grabbing the right region object first – based on the index path section, and then the course object itself – based on the index path row.
As to the data communication between the master and detail controllers, I took a simplified approach compared to Nerdfeed. Here we don’t have to do with alternative view controllers in the detail pane, so we don’t need a protocol like ListViewControllerDelegate in order to abstract from their different types. It is sufficient to pass a course object to the detail view controller when it is reused and have its table view reload the new data.


#3

@alberto :

Hi,
Could you give us more details on your “readFromJSONDictionary:” (or whatever you called it) method ?
I like the idea of not hardcoding the name of regions in the Json dictionary, but I’m puzzled by the fact that there are no arrays in the BNR schedule JSON file : it’s a hierarchy of objects that I don’t know how to parse …

Thanks
Fred


#4

Hi Fred, what follows is a description of my solution – as far the parsing of BNR schedule JSON data is concerned.

I put up a root class adopting the JSONSerializable protocol (a sort of RSSChannel) declaring only an ‘items’ mutable array (to hold region objects, as you will immediately see). Its readFromJSONDictionary: method is as follows:

[code]// This method gets passed the ‘main’ dictionary retrieved in BNRConnection’s
// connectionDidFinishLoading: (through NSJSONSerialization’s JSONObjectWithData:
// options:error: method)

  • (void)readFromJSONDictionary:(NSDictionary *)d
    {
    // Create a sorted array of the dictionary keys – currently the array will
    // contain the strings @“EUROPE” and @"UNITED STATES"
    NSArray *keys = [[d allKeys] sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
    return [obj1 compare:obj2];
    }];

    // For each key/region
    for (NSString *key in keys) {
    // Get the corresponding dictionary – it contains the courses scheduled
    // for that region
    NSDictionary *dict = [d objectForKey:key];

      // Create a region object, add it to items array, set its name, and pass
      // the courses dictionary to its readFromJSONDictionary: method
      JCBNRCRegion *region = [[JCBNRCRegion alloc] init];
      [[self items] addObject:region];
      [region setName:key];
      [region readFromJSONDictionary:dict];
    

    }
    }[/code]

The JCBNRCRegion class (JSONSerializable) declares a ‘name’ property (set in the code above), and declares in turn a mutable array of course objects (‘classes’). Its readFromJSONDictionary: method is responsible of populating this array:

[code]- (void)readFromJSONDictionary:(NSDictionary *)d
{
// Create an array of the scheduled classes and sort it by date
// – something better could be done here, but I was in a hurry…
NSArray *classDictionaries = [[d allValues] sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
NSString *date1 = [(NSDictionary *)obj1 objectForKey:@“class_begins”];
NSString *date2 = [(NSDictionary *)obj2 objectForKey:@“class_begins”];
return [date1 compare:date2];
}];

// For each class
for (NSDictionary *classDictionary in classDictionaries) {
    // Create a JCBNRCClass object, add it to the classes array, and
    // pass the class details dictionary to its readFromJSONDictionary:
    // method
    JCBNRCClass *clazz = [[JCBNRCClass alloc] init];
    [[self classes] addObject:clazz];
    [clazz readFromJSONDictionary:classDictionary];
}

}[/code]

Finally, the JCBNRCClass class (JSONSerializable) declares properties (‘schedule_id’, ‘class_begins’, and so on) for some of the course details in the JSON data. Its readFromJSONDictionary: method is relatively straightforward. Its responsibility is to read a leaf dictionary and load the object with those details:

- (void)readFromJSONDictionary:(NSDictionary *)d { [self setSchedule_id:[[d objectForKey:@"schedule_id"] intValue]]; NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; [formatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]]; [formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss ZZZZ"]; NSDate *beginDate = [formatter dateFromString:[d objectForKey:@"class_begins"]]; [self setClass_begins:beginDate]; NSDate *endDate = [formatter dateFromString:[d objectForKey:@"class_ends"]]; [self setClass_ends:endDate]; [self setPrice:[[d objectForKey:@"price"] floatValue]]; [self setTitle:[d objectForKey:@"title"]]; [self setInstructor_one:[d objectForKey:@"instructor_one"]]; id instructor2 = [d objectForKey:@"instructor_two"]; [self setInstructor_two:instructor2 == [NSNull null] ? @"" : instructor2]; [self setLocality:[d objectForKey:@"locality"]]; [self setRegion:[d objectForKey:@"region"]]; [self setCurrency:[[d objectForKey:@"currency"] intValue]]; }

In my previous post I also (summarily) described how to leverage this simple data model to have our parsed data display in the table views.


#5

Wow, great, thanks !

As a coincidence, I found out just today how to parse the JSON file … tough !
I will post it, because it’s another way to do it, but I wanted to say thanks for sharing … it really helps
:slight_smile:

Fred


#6

So here is the solution I implemented :
I chose to keep RSSItem and RSSChannel classes.
I just added a new property to RSSChannel called “regions” and another called “courses”. And I kept the “items”. So here are the 3 levels corresponding to the JSON file : regions, courses, items. All defined as arrays in RSSChannel.h :

#import <Foundation/Foundation.h>
#import "JSONSerializable.h"

@interface RSSChannel : NSObject <JSONSerializable>

@property (nonatomic, readonly, strong) NSArray *regions;
@property (nonatomic, readonly, strong) NSArray *courses;
@property (nonatomic, readonly, strong) NSMutableArray *items;

@end

I updated RSChannel.m this way :

[code]- (void)readFromJSONDictionary:(NSDictionary *)d{

regions = [d allKeys]; //NSArray of dictionary's keys
NSLog(@"Regions : %@", regions);
    
for (NSDictionary *region in regions) {
    NSLog(@"Current region : %@", region);
    
    courses = [d objectForKey:region];
    NSLog(@"Courses : %@", courses);

    for (NSDictionary *course in courses) {
        NSLog(@"Current course: %@", course);
        RSSItem *i = [[RSSItem alloc] init];
        
        // Pass the entry dictionary to the item so it can grab its ivars
        [i readFromJSONDictionary:[[d objectForKey:region] objectForKey:course]];
        
        [[self items] addObject:i];
    }
    
}

// Sort the array of items by classBegins date ascending
[[self items] sortUsingComparator:^NSComparisonResult(id obj1, id obj2) {
    return [[obj1 classBegins] compare:[obj2 classBegins]];
}];

}
[/code]

Here is my RSSItem.h :

[code]#import <Foundation/Foundation.h>
#import “JSONSerializable.h”

@interface RSSItem : NSObject

@property (nonatomic, strong) NSString *title;
@property (nonatomic, strong) NSNumber *scheduleID;
@property (nonatomic, strong) NSDate *classBegins;
@property (nonatomic, strong) NSDate *classEnds;
@property (nonatomic) float price;
@property (nonatomic) int currency;
@property (nonatomic, strong) NSNumber *totalSpaces;
@property (nonatomic, strong) NSString *instructor1;
@property (nonatomic, strong) NSString *instructor2;
@property (nonatomic, strong) NSString *locality;
@property (nonatomic, strong) NSString *region;

@end
[/code]

And my RSSItem.m

[code]#import “RSSItem.h”

@implementation RSSItem

@synthesize title, scheduleID, classBegins, classEnds, price, currency, totalSpaces, instructor1, instructor2, locality, region;

  • (void)readFromJSONDictionary:(NSDictionary *)d {

    [self setTitle:[d objectForKey:@“title”]];
    [self setScheduleID:[d objectForKey:@“schedule_id”]];

    // dates
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    [formatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@“en_US_POSIX”]];
    [formatter setDateFormat:@“yyyy-MM-dd HH:mm:ss ZZZZ”];
    NSDate *beginDate = [formatter dateFromString:[d objectForKey:@“class_begins”]];
    [self setClassBegins:beginDate];
    NSDate *endDate = [formatter dateFromString:[d objectForKey:@“class_ends”]];
    [self setClassEnds:endDate];

    [self setPrice:[[d objectForKey:@“price”]floatValue]];
    [self setCurrency:[[d objectForKey:@“currency”]intValue]];
    [self setTotalSpaces:[d objectForKey:@“total_spaces”]];
    [self setInstructor1:[d objectForKey:@“instructor_one”]];

    // instructor 2
    id instructor_two = [d objectForKey:@“instructor_two”];
    [self setInstructor2:instructor_two == [NSNull null] ? @"" : instructor_two];

    [self setLocality:[d objectForKey:@“locality”]];
    [self setRegion:[d objectForKey:@“region”]];

    NSLog(@"%@ has property ID %@", [self title], [self scheduleID]);

}

@end[/code]

That probably makes my ListViewController a bit more complicated, as I need to know at all times to which region belongs the course I’m showing :
ListViewController.h

[code]#import <Foundation/Foundation.h>

@class DetailViewController;
@class RSSChannel;

@interface ListViewController : UITableViewController
{
RSSChannel *channel;

}

@property (nonatomic, strong) DetailViewController *detailViewController;

  • (void)fetchEntries;

@end
[/code]

ListViewcontroller.m

[code]#import “ListViewController.h”
#import “DetailViewController.h”
#import “RSSChannel.h”
#import “RSSItem.h”
#import “BNRFeedStore.h”

@implementation ListViewController

@synthesize detailViewController;

  • (id)initWithStyle:(UITableViewStyle)style
    {
    self = [super initWithStyle:style];

    if (self) {

      [[self navigationItem] setTitle:@"BNR Classes"];
    
      [self fetchEntries];
    

    }
    return self;
    }

  • (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
    {
    return [[channel regions] count];
    }

  • (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
    {
    NSString *sectionTitle ;
    sectionTitle = [[channel regions] objectAtIndex:section] ;
    return sectionTitle;
    }

  • (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
    {
    // NSMutableArray *itemsInRegion = [[NSMutableArray alloc] init];
    // for (RSSItem *i in [channel items]) {
    // if ([i region] == [[channel regions] objectAtIndex:section])
    // [itemsInRegion addObject:i];
    // }

    // With a predicate
    NSString *propertyKey = @“region”;
    NSString *attributeValue = [[channel regions] objectAtIndex:section];
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K like %@",
    propertyKey, attributeValue];

    NSArray *itemsInRegion = [[channel items] filteredArrayUsingPredicate:predicate];

    return [itemsInRegion count];
    }

  • (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@“UITableViewCell”];
    if (cell == nil) {
    cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@“UITableViewCell”];
    }

    // Predicate
    NSString *propertyKey = @“region”;
    NSString *attributeValue = [[channel regions] objectAtIndex:[indexPath section]];
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K like %@",
    propertyKey, attributeValue];

    NSArray *itemsInRegion = [[channel items] filteredArrayUsingPredicate:predicate];

    RSSItem *item = [itemsInRegion objectAtIndex:[indexPath row]];

    // Cell title
    [[cell textLabel] setText:[item title]];

    // Cell subtitle
    NSDateFormatter *formatter = [[NSDateFormatter alloc]init];
    [formatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@“en_US_POSIX”]];
    [formatter setDateStyle:NSDateFormatterMediumStyle];
    NSString *subtitle = [NSString stringWithFormat:@"%@ - %@",[item locality], [formatter stringFromDate:[item classBegins]]];
    [[cell detailTextLabel] setText];

    return cell;
    }

  • (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
    {

    if(![self splitViewController])
    // Push the detail view controller onto the navigation stack

      [[self navigationController] pushViewController:detailViewController
                                             animated:YES];
    

    // Grab the selected item
    // Predicate
    NSString *propertyKey = @“region”;
    NSString *attributeValue = [[channel regions] objectAtIndex:[indexPath section]];
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K like %@",
    propertyKey, attributeValue];

    NSArray *itemsInRegion = [[channel items] filteredArrayUsingPredicate:predicate];

    RSSItem *entry = [itemsInRegion objectAtIndex:[indexPath row]];

    [[detailViewController navigationItem] setTitle:@“Class info”];
    [detailViewController setItem:entry];
    [[detailViewController tableView]reloadData];

}

  • (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
    {
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
    return YES;
    } else {
    return (interfaceOrientation == UIInterfaceOrientationPortrait);

    }
    }

  • (void)fetchEntries;
    {
    // Get ahold of the segmented control that is currently in the title view
    UIView *currentTitleView = [[self navigationItem] titleView];

    // Create an activity indicator and start it spinning in the nav bar
    UIActivityIndicatorView *aiView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
    [[self navigationItem] setTitleView:aiView];
    [aiView startAnimating];

    void(^completionBlock)(RSSChannel *obj, NSError *err) =
    ^(RSSChannel *obj, NSError *err) {

      // When the request completes - success or failure - , replace the activity
      // indicator with the segmented control
      [[self navigationItem] setTitleView:currentTitleView];
      
      if (!err) {
          // If everything went OK, grab the channel object, and
          // reload the table.
          channel = obj;
          
          [[self tableView] reloadData];
      } else {
          
          // If things went bad, show an alert view
          NSString *errorString = [NSString stringWithFormat:@"Fetch failed: %@", [err localizedDescription]];
          
          UIAlertView *av = [[UIAlertView alloc]
                             initWithTitle:@"Error"
                             message:errorString
                             delegate:nil
                             cancelButtonTitle:@"OK"
                             otherButtonTitles:nil];
          
          [av show];
      }
    

    }; // end of block

    // Initiate the request …
    [[BNRFeedStore sharedStore] fetchRSSFeedWithCompletion:completionBlock];

}

@end
[/code]

And here is the DetailViewController. Like Alberto, I thought it was not necessary in this case to have a ListViewControllerDelegate protocol, because there is only one detail controller.

DetailViewController.h

[code]#import <UIKit/UIKit.h>
#import “ListViewController.h”

@class RSSItem;

@interface DetailViewController : UITableViewController

@property (nonatomic, strong) RSSItem *item;

@end[/code]

DetailViewController.m

[code]#import “DetailViewController.h”
#import “RSSItem.h”

@interface DetailViewController ()

@end

@implementation DetailViewController

@synthesize item;

  • (id)initWithStyle:(UITableViewStyle)style
    {
    self = [super initWithStyle:style];

    if (self) {

      // Nothing special
    

    }
    return self;
    }

  • (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
    {
    return 1;
    }

  • (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
    {
    return [item title];
    }

  • (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
    {
    return 10;
    }

  • (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@“UITableViewCell”];
    if (cell == nil) {
    cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:@“UITableViewCell”];
    }

    switch ([indexPath row]) {
    case 0: {
    [[cell textLabel] setText:@“Class Title”];
    [[cell detailTextLabel] setText:[item title]];
    break;
    }

      case 1: {
          [[cell textLabel] setText:@"Class ID"];
          if ([item scheduleID] != 0)
              [[cell detailTextLabel] setText:[NSString stringWithFormat:@"%@",[item scheduleID]]];
          break;
      }
          
      case 2: {
          [[cell textLabel] setText:@"Class begins"];
          NSDateFormatter *classBeginsFormatter = [[NSDateFormatter alloc]init];
          [classBeginsFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];
          [classBeginsFormatter setDateStyle:NSDateFormatterMediumStyle];
          [[cell detailTextLabel] setText:[classBeginsFormatter stringFromDate:[item classBegins]]];
          break;
      }
          
      case 3: {
          [[cell textLabel] setText:@"Class ends"];
          NSDateFormatter *classEndsformatter = [[NSDateFormatter alloc]init];
          [classEndsformatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];
          [classEndsformatter setDateStyle:NSDateFormatterMediumStyle];
          [[cell detailTextLabel] setText:[classEndsformatter stringFromDate:[item classEnds]]];
          break;
      }
          
      case 4: {
          [[cell textLabel] setText:@"Instructor 1"];
          [[cell detailTextLabel] setText:[item instructor1]];
          break;
      }
          
      case 5:
          [[cell textLabel] setText:@"Instructor 2"];
          [[cell detailTextLabel] setText:[item instructor2]];
          break;
          
          
      case 6: {
          [[cell textLabel] setText:@"Price"];
          
          NSString *currencyCode = [[NSString alloc] init];
          switch ([item currency]) {
              case 0:
                  currencyCode = @"USD";
                  break;
                  
              case 1:
                  currencyCode = @"EUR";
                  break;
                  
              default:
                  break;
          }
          
          NSNumberFormatter *priceFormatter = [[NSNumberFormatter alloc] init];
          [priceFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]];
          [priceFormatter setNumberStyle: NSNumberFormatterDecimalStyle];
          NSNumber *priceDecimal = [NSNumber numberWithFloat:[item price]];
          NSString *priceDecimalString = [priceFormatter stringFromNumber:priceDecimal];
          
          if ([item price] != 0)
              [[cell detailTextLabel] setText:[NSString stringWithFormat:@"%@ %@",currencyCode, priceDecimalString]];
          break;
      }
          
          
      case 7: {
          [[cell textLabel] setText:@"Available spaces"];
          if ([item totalSpaces] != 0)
              [[cell detailTextLabel] setText:[NSString stringWithFormat:@"%@",[item totalSpaces]]];
          break;
      }
          
      case 8: {
          [[cell textLabel] setText:@"Location"];
          [[cell detailTextLabel] setText:[item locality]];
          break;
      }
          
      case 9: {
          [[cell textLabel] setText:@"Region"];
          [[cell detailTextLabel] setText:[item region]];
          break;
      }
      
      default:
          break;
    

    }

    return cell;
    }

  • (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
    {
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
    return YES;
    } else {
    return (interfaceOrientation == UIInterfaceOrientationPortrait);

    }
    }

  • (void)splitViewController:(UISplitViewController *)svc
    willHideViewController:(UIViewController *)aViewController
    withBarButtonItem:(UIBarButtonItem *)barButtonItem
    forPopoverController:(UIPopoverController *)pc

{
// If this bar button item deosn’t hava a title, it won’t appear at all
[barButtonItem setTitle:@“List”];

// Take this bar button item and put it on the left side of our nav item
[[self navigationItem] setLeftBarButtonItem:barButtonItem];

}

  • (void)splitViewController:(UISplitViewController *)svc
    willShowViewController:(UIViewController *)aViewController
    invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem
    {
    // Remove the bar button item from our navigation item
    // We’ll double check that it’s the correct button, though we know it is
    if (barButtonItem == [[self navigationItem]leftBarButtonItem])
    [[self navigationItem] setLeftBarButtonItem:nil];
    }

@end
[/code]

The rest is simply based on the chapter in the book.
The final result looks like Alberto’s one, even if the structure is a bit different.

Thanks to Alberto and BryanLuby for sharing their ideas. It helped me when I was stuck.

Fred

[EDIT : corrected a design error in ListViewController.m … that solved the problem I had in portrait mode with my “List” button]


#7

Any chance on getting the whole source code?