Mega Gold Challenge Solution and Question


#1

This is my solution. I decided not to have the equivalent of both an RSSItem and RSSChannel because it seemed pointless in this case so just used the one class BNRClassTimes. If you look at my code you see I create an instance of BNRClassTimes within BNRClassTImes and it works without a problem. My question is - is creating an instance of a class within that same class a good idea or can it cause any issues in some cases?

Also thanks to BrianLuby http://forums.bignerdranch.com/viewtopic.php?f=240&t=4453 for getting the date from the JSON feed, that caused me the main issue in this challenge.

ListViewController.h

[code]#import <Foundation/Foundation.h>
@class ClassDetailViewController;
@class BNRClassTimes;

@interface ListViewController : UITableViewController
{
BNRClassTimes *classTimes;
}

@property (nonatomic, strong) ClassDetailViewController *classDetailViewController;
@end

//a new protocol named ListViewControllerDelegate
@protocol ListViewControllerDelegate

//classes that conform to this protocol must implement this method
-(void)listViewController:(ListViewController *)lvc handleObject:(id)object;

@end[/code]

ListViewController.m

[code]#import “ListViewController.h”
#import “ClassDetailViewController.h”
#import “BNRFeedStore.h”
#import “BNRClassTimes.h”

@implementation ListViewController
@synthesize classDetailViewController;

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

if (self) {
    
    [[self navigationItem] setTitle:@"BNR Classes"];
    [self fetchEntries];
    
}
return self;

}

  • (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
    {
    return [[classTimes items]count];
    }

  • (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
    UITableViewCell *cell = [tableView
    dequeueReusableCellWithIdentifier:@“UITableViewCell”];
    if (cell == nil) {
    cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
    reuseIdentifier:@“UITableViewCell”];
    }
    BNRClassTimes *class = [[classTimes items] objectAtIndex:[indexPath row]];
    [[cell textLabel] setText:[class title]];

    NSDateFormatter *format = [[NSDateFormatter alloc] init];
    [format setDateStyle:NSDateFormatterMediumStyle];
    [format setTimeStyle:NSDateFormatterNoStyle];

    NSString *begin= [format stringFromDate:[class dateStart]];
    NSString *end= [format stringFromDate:[class dateFinish]];

    NSString *dates = [NSString stringWithFormat:@"%@ - %@", begin, end];
    [[cell detailTextLabel] setText:dates];

    return cell;
    }

  • (void)tableView:(UITableView *)tableView
    didSelectRowAtIndexPath:(NSIndexPath *)indexPath
    {
    //if not in a split view controller (when using iPhone) then push the webviewcontroller onto navigation controller stack. If in splitviewcontroler (using iPad) then just let the uiSplitViewController to place the webviewcontroller on screen
    if (![self splitViewController]) {
    // Push the web view controller onto the navigation stack - this implicitly
    // creates the web view controller’s view the first time through
    [[self navigationController] pushViewController:classDetailViewController animated:YES];
    }
    else {
    // We have to create a new navigation controller, as the old one
    // was only retained by the split view controller and is now gone
    UINavigationController *nav =
    [[UINavigationController alloc] initWithRootViewController:classDetailViewController];

      NSArray *vcs = [NSArray arrayWithObjects:[self navigationController],
                      nav,
                      nil];
      
      [[self splitViewController] setViewControllers:vcs];
      
      // Make the detail view controller the delegate of the split view controller
      [[self splitViewController] setDelegate:classDetailViewController];
    

    }

    // Grab the selected item
    BNRClassTimes *classDetail = [[classTimes items] objectAtIndex:[indexPath row]];
    [classDetailViewController listViewController:self handleObject:classDetail];
    }

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

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

      [[self navigationItem] setTitleView:currentTitleView];
      
      if (!err) {
          //if everything went ok, grab the classTimes object, and relod the table.
          classTimes = obj;
          
          [[self tableView]reloadData];
          
          //this adds details to the classDetailViewController when it loads initially so it isn't populated with null and blank values
          BNRClassTimes *classDetail = [[classTimes items] objectAtIndex:0];
          [classDetailViewController listViewController:self handleObject:classDetail];
          
      } 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];
      }
    

    };

    //initiate the request
    [[BNRFeedStore sharedStore]fetchClassDetailsWithCompletion:completionBlock];

}

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

@end[/code]

ClassDetailViewController.h

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

@class BNRClassTimes;

@interface ClassDetailViewController : UITableViewController <UISplitViewControllerDelegate, ListViewControllerDelegate>
{
BNRClassTimes *classTimes;
}

@end
[/code]

ClassDetailViewController.m

[code]#import “ClassDetailViewController.h”
#import “BNRClassTimes.h”

@implementation ClassDetailViewController

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

if (self) {
    
    [[self navigationItem] setTitle:@"BNR Class Details"];
    
}
return self;

}

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

  • (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
    UITableViewCell *cell =
    [tableView dequeueReusableCellWithIdentifier:@“UITableViewCell”];

    if (!cell)
    cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue2
    reuseIdentifier:@“UITableViewCell”];

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

      case 1: {
          [[cell textLabel] setText:@"Dates"];
          
          //format the dates into easier to read style
          NSDateFormatter *format = [[NSDateFormatter alloc] init];
          [format setDateStyle:NSDateFormatterMediumStyle];
          [format setTimeStyle:NSDateFormatterNoStyle];
          
          NSString *begin= [format stringFromDate:[classTimes dateStart]];
          NSString *end= [format stringFromDate:[classTimes dateFinish]];
    
          
          NSString *dates = [NSString stringWithFormat:@"%@ - %@", begin, end];
          [[cell detailTextLabel] setText:dates];
          break;
      }
          
      case 2:{
          [[cell textLabel] setText:@"Instructor"];
          //not always a 2nd instructor so this takes care of cases where there are 1 or 2 instructors
          if ([[classTimes instructor2]isKindOfClass:[NSNull class]]) {
              [[cell detailTextLabel] setText:[classTimes instructor]];
          } else {
              NSString *instructor = [NSString stringWithFormat:@"%@ / %@", [classTimes instructor], [classTimes instructor2]];
              [[cell detailTextLabel] setText:instructor];
          }
          break;
      }
          
      case 3:{
          [[cell textLabel] setText:@"Region"];
          [[cell detailTextLabel] setText:[classTimes region]];
          break;
      }
      
      case 4: {
          [[cell textLabel] setText:@"Locality"];
          [[cell detailTextLabel] setText:[classTimes locality]];
          break;
      }
          
      case 5: {
          [[cell textLabel] setText:@"Spaces"];
          if ([[classTimes spaces]isEqual:@"__null"]) {
              [[cell detailTextLabel] setText:[NSString stringWithFormat:@"0"]];
          } else {
              [[cell detailTextLabel] setText:[NSString stringWithFormat:@"%@",[classTimes spaces]]];
          }
          break;
      }
          
      case 6: {
          [[cell textLabel] setText:@"Cost"];
          [[cell detailTextLabel] setText:[classTimes price]];
      }
      
      default:
          break;
    

    }
    return cell;
    }

-(void)listViewController:(ListViewController *)lvc handleObject:(id)object
{
// Make sure the ListViewController gave us the right object
if (![object isKindOfClass:[BNRClassTimes class]]) {
return;
}

classTimes = object;

[[self tableView] reloadData];

}

  • (void)splitViewController:(UISplitViewController *)svc
    willHideViewController:(UIViewController *)aViewController
    withBarButtonItem:(UIBarButtonItem *)barButtonItem
    forPopoverController:(UIPopoverController *)pc
    {
    // If this bar button item doesn’t have 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 *)button
    {
    // Remove the bar button item from our navigation item
    // We’ll double check that its the correct button, even though we know it is
    if (button == [[self navigationItem] leftBarButtonItem])
    [[self navigationItem] setLeftBarButtonItem:nil];
    }

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

@end
[/code]

BNRClassTimes.h

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

@interface BNRClassTimes : NSObject

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

@property (nonatomic, strong) NSString *title;
@property (nonatomic, strong) NSNumber *spaces;
@property (nonatomic, strong) NSString *instructor;
@property (nonatomic, strong) NSString *instructor2;
@property (nonatomic, strong) NSString *region;
@property (nonatomic, strong) NSString *locality;
@property (nonatomic, strong) NSDate *dateStart;
@property (nonatomic, strong) NSDate *dateFinish;
@property (nonatomic, strong) NSString *price;

  • (NSString *)formatDate:(NSString *)JSONDate;
  • (NSString *)cost:(NSNumber )cost withRegionLocation:(NSString)country;

@end
[/code]

BNRClassTimes.m

[code]#import “BNRClassTimes.h”

@implementation BNRClassTimes
@synthesize items, title, spaces, instructor, region, locality, dateStart, dateFinish, price;

  • (id)init
    {
    self = [super init];

    if (self) {
    // Create the container for the RSSItems this channel has;
    // we’ll create the RSSItem class shortly.
    items = [[NSMutableArray alloc] init];
    }

    return self;
    }

  • (void)readFromJSONDictionary:(NSDictionary *)d
    {
    //This is less straightforward because there are no arrays in the big nerd ranch json feed.
    for (NSDictionary *country in d) {
    //find the name of each country key by using description at the start of the json feed and then make a dictionary out of that. This is more flexible than hard coding in United States, etc
    NSString *countryName = [country description];
    NSDictionary *countryDictionary = [d objectForKey:countryName];

      for (NSDictionary *class in countryDictionary) {
          //same idea here, get the class code for each countryDictionary rather than having to hard code values
          NSString *classCode = [class description];
          NSDictionary *classDictionary = [countryDictionary objectForKey:classCode];
          
          BNRClassTimes *theClass = [[BNRClassTimes alloc]init];
          
          [theClass setTitle:[classDictionary objectForKey:@"title"]];
          [theClass setSpaces:[classDictionary objectForKey:@"total_spaces"]];
          [theClass setInstructor:[classDictionary objectForKey:@"instructor_one"]];
          [theClass setInstructor2:[classDictionary objectForKey:@"instructor_two"]];
          [theClass setRegion:[classDictionary objectForKey:@"region"]];
          [theClass setLocality:[classDictionary objectForKey:@"locality"]];
          [theClass setDateStart:[self formatDate:[classDictionary objectForKey:@"class_begins"]]];
          [theClass setDateFinish:[self formatDate:[classDictionary objectForKey:@"class_ends"]]];
          [theClass setPrice:[self cost:[classDictionary objectForKey:@"price"] withRegionLocation:[classDictionary objectForKey:@"region"]]];
          
          [items addObject];
      }
    

    }
    //once items is populated with data, sort it by the start date
    NSSortDescriptor *sortByDate = [[NSSortDescriptor alloc] initWithKey:@“dateStart” ascending:TRUE];
    [items sortUsingDescriptors:[NSArray arrayWithObject:sortByDate]];
    }

//formats the date from the JSON feed into NSDate. Kept it as NSDate rather than convert to string to make the array easier to sort and also easier to display different date formats in the list and classDetail view controllers

  • (NSDate *)formatDate:(NSString *)JSONDate
    {
    NSDateFormatter *format = [[NSDateFormatter alloc] init];
    [format setDateFormat:@“yyyy-MM-dd HH:mm:ss Z”];
    NSDate *dateFormatted = [format dateFromString];

    return dateFormatted;
    }

//depending on the country a currency symbol is tagged onto the front of the string

  • (NSString *)cost:(NSNumber )cost withRegionLocation:(NSString)country
    {
    if ([country isEqualToString:@“UNITED STATES”]) {
    return [NSString stringWithFormat:@"$%@", cost];

    } else if ([country isEqualToString:@“EUROPE”]) {
    return [NSString stringWithFormat:@"\u20ac%@", cost];

    } else {
    return [NSString stringWithFormat:@"%@", cost];
    }
    }
    @end
    [/code]