Gold Challenge Questions


#1

About that Gold Challenge…

I’m wondering if anybody in the class would like to share their high-level approach to this challenge.
The challenge itself is a big vague

Mostly I’m interested in how folks decided to display the thread detail.
There are at least a couple obvious approaches and one a bit less obvious (to me):

  1. Show the parent and detail items in a new table pushed onto the master navigation controller stack.
  2. Show the detail items in a table displayed as a popup pointing to the parent item.
  3. Increase the size of the master table to include the detail items which are displayed after the thread parent – probably in some visibly-distinguishable style.

#1 seems the most straightforward, but to then display the web link detail by bonking on the table items requires, presumably, duplicating all the other behavior in ListViewController. That’s easy enough, if annoying, but it doesn’t feel like a good solution because it makes maintenance harder. Duplicated code. Bleach!

#2 has the same problems as #1 with the additional problem of the popup obscuring the web (or info) stuff.

#3 seems like a good thing to do, intuitive and not requiring duplicated code, but I’m not sure how to control it.
To get a touch-tracking accessory “button” in a UITableViewCell you have to use the UITableViewCellAccessoryDetailDisclosureButton.
That’s OK, but once you expand the table to show the thread detail entries, what do you with the accessory?
Invent a new accessory view and do all the stuff to get touches and have actions?
It just seems like there should be a better and simpler way. That usually means I’m missing something fundamental.
Perhaps one of you can see what that is…


#2

I didn’t complete this challenge yet, but I suppose we should get inspiration from the mail app on the iPad, and in particular the way it “groups” threads of mails.
At least, that is the direction I’m going to take.
If I ever succeed in completing this one, I’ll post it.

Fred


#3

Hi:
the data is inconsistent with the format stated in the text, i.e., .* :: .* :: .*
therefore, to avoid overly complicating the reg expression, i created 2 regular expressions, whereby, if the 1st one fails i then try the other etc…

In addition, i went ahead and created another table representing the replies and presented that on the nav stack; this works well on either iPad or iPhone, i.e., same cosmetics as previous
nav views. This ‘replies’ table view is triggered via the disclosure button on the parent list…

I’ll also attempt to solve this another way, i.e., using a drop-down child table on the parent table… thus avoiding a separate nav view…


#4

Great ! Could you post your solution ?

Thanks !


#5

Just about to start this.

There is a Disclosure Button style for UITableViewCell, so my plan is to use this to show/hide all the thread children

I’ll let you know how it goes


#6

That works.

So I’ll give a summary, it took me longer than I thought and now need to go to bed :slight_smile:

Made a new class RSSThread that contains a NSMutable array of RSSItems.

RSSChannel has an NSMutableArray of RSSThread objects;

This is populated when I trim the titles, so if the title doesn’t start with "Re: " I assume it’s a new thread.

If the title starts with "Re: " I search the array of threads, and if I don’t find a match then I start a new thread.

If I have a match then I add the item to the array of items in the thread. The lead item on a thread has a flag indicating it is the lead item and a pointer to the thread object;

So having refined the model I then have to do this in the ListViewController

numberOfRowsInSection now looks like this

[code]- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
int rows = 0;

for (RSSThread *t in [channel threads]) {
    //add a row for the thread
    rows++;
    
    //and add rows for each reply
    if ([t showItems]) {
        rows+=[[t items] count] - 1;
    }
}
return rows;

}
[/code]

have to keep track of which rows are visible so I wrote this

- (RSSItem*)itemAtRow:(int)row
{
    int nextRow = 0;
    int lastRow = 0;
    
    for (RSSThread *t in [channel threads]) {
        
        if ([t showItems]) {
            nextRow += [[t items] count];
        }
        else {
            nextRow++;
        }
        
        if (nextRow > row) {
            //this thread contains the item
            return [[t items] objectAtIndex:row - lastRow];
        }
        
        lastRow = nextRow;
    }
    return nil;   
}

now cellForRowAtIndexPath looks like this

- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
     
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"UITableViewCell"];
    
    if( !cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1
                                      reuseIdentifier:@"UITableViewCell"];
    }
    
    RSSItem *item = [self itemAtRow:[indexPath row]];
    if ([item isThreadStart] && [[[item thread] items]count] > 1 ) {
        [cell setAccessoryType:UITableViewCellAccessoryDetailDisclosureButton];
        [[cell textLabel] setText:[item title]];
    }
    else {
        if (![item isThreadStart]) {
            [cell setIndentationLevel:1];
        }
        [cell setAccessoryType:UITableViewCellAccessoryNone];
        [[cell detailTextLabel] setText:[item subForum]];
        [[cell textLabel] setText:[item title]];
    }
    return cell;
}

didSelectRowAtIndexPath looks like this

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (![self splitViewController]) {
        // push the webViewController onto the navigation stack
        [[self navigationController] pushViewController:webViewController animated:YES];
    } else {
        UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:webViewController];
        NSArray *vcs = [NSArray arrayWithObjects:[self navigationController], nav, nil];
        [[self splitViewController] setViewControllers:vcs];
        [[self splitViewController] setDelegate:webViewController];
    }
    
    //grab the selected item
    RSSItem *entry = [self itemAtRow:[indexPath row]];
    
    [webViewController listViewController:self
                             handleObject:entry];
}

and we have this delegate to toggle the display of the thread replies

- (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath
{
    NSLog(@"Tapped row %d", [indexPath row]);
    //find the thread
    RSSThread *thisThread = [[self itemAtRow:[indexPath row]] thread];
    [thisThread setShowItems:![thisThread showItems]];
    [[self tableView] reloadData];
}

#7

i am having issues with getting the gold challenge to work, can you please provide your RSSThread to get your solution working

thanks


#8

Is your syntax correct? When I try:

for (RSSThread *t in [channel threads])

I get an error: “Use of undeclared identifier t”

[quote=“lennieh”]That works.

So I’ll give a summary, it took me longer than I thought and now need to go to bed :slight_smile:

Made a new class RSSThread that contains a NSMutable array of RSSItems.

RSSChannel has an NSMutableArray of RSSThread objects;

This is populated when I trim the titles, so if the title doesn’t start with "Re: " I assume it’s a new thread.

If the title starts with "Re: " I search the array of threads, and if I don’t find a match then I start a new thread.

If I have a match then I add the item to the array of items in the thread. The lead item on a thread has a flag indicating it is the lead item and a pointer to the thread object;

So having refined the model I then have to do this in the ListViewController

numberOfRowsInSection now looks like this

[code]- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
int rows = 0;

for (RSSThread *t in [channel threads]) {
    //add a row for the thread
    rows++;
    
    //and add rows for each reply
    if ([t showItems]) {
        rows+=[[t items] count] - 1;
    }
}
return rows;

}
[/code]

have to keep track of which rows are visible so I wrote this

- (RSSItem*)itemAtRow:(int)row
{
    int nextRow = 0;
    int lastRow = 0;
    
    for (RSSThread *t in [channel threads]) {
        
        if ([t showItems]) {
            nextRow += [[t items] count];
        }
        else {
            nextRow++;
        }
        
        if (nextRow > row) {
            //this thread contains the item
            return [[t items] objectAtIndex:row - lastRow];
        }
        
        lastRow = nextRow;
    }
    return nil;   
}

now cellForRowAtIndexPath looks like this

- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
     
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"UITableViewCell"];
    
    if( !cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1
                                      reuseIdentifier:@"UITableViewCell"];
    }
    
    RSSItem *item = [self itemAtRow:[indexPath row]];
    if ([item isThreadStart] && [[[item thread] items]count] > 1 ) {
        [cell setAccessoryType:UITableViewCellAccessoryDetailDisclosureButton];
        [[cell textLabel] setText:[item title]];
    }
    else {
        if (![item isThreadStart]) {
            [cell setIndentationLevel:1];
        }
        [cell setAccessoryType:UITableViewCellAccessoryNone];
        [[cell detailTextLabel] setText:[item subForum]];
        [[cell textLabel] setText:[item title]];
    }
    return cell;
}

didSelectRowAtIndexPath looks like this

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (![self splitViewController]) {
        // push the webViewController onto the navigation stack
        [[self navigationController] pushViewController:webViewController animated:YES];
    } else {
        UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:webViewController];
        NSArray *vcs = [NSArray arrayWithObjects:[self navigationController], nav, nil];
        [[self splitViewController] setViewControllers:vcs];
        [[self splitViewController] setDelegate:webViewController];
    }
    
    //grab the selected item
    RSSItem *entry = [self itemAtRow:[indexPath row]];
    
    [webViewController listViewController:self
                             handleObject:entry];
}

and we have this delegate to toggle the display of the thread replies

[code]

  • (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath
    {
    NSLog(@“Tapped row %d”, [indexPath row]);
    //find the thread
    RSSThread *thisThread = [[self itemAtRow:[indexPath row]] thread];
    [thisThread setShowItems:![thisThread showItems]];
    [[self tableView] reloadData];
    }
    [/code][/quote]

#9

This problem took me ages and ages to figure out how to do as I pictured it in my mind. Thanks to lennieh for giving me the idea to use a separate class to hold the thread. I would have never thought of that on my own. I went a bit further. I didn’t hide the replies in the main ListViewController, and instead I chose to load the replies in another view that I pushed onto the ListViewController.

To do this, I created two new classes:
–RSSReplies.h/.m
–ParentAndRepliesViewController.h/m

The ParentAndRepliesViewController is a subclass of UITableViewController, and I modeled it almost exactly like the ListViewController. It has three properties for a webViewController, an RSSChannel, and an RSSReplies object.

The RSSReplies class is basically a NSMutableArray with nested arrays. The class contains a “store” array and methods to edit the store (think of the Homepwner allItems). The mutableArray contains threads (an array of parent and reply posts). The RSSChannel class populates the thread arrays via the trimTitles method. And I hope that wasn’t too confusing.

The other major changes occurred in the ListViewController. I made a separate RSSReplies ivar there and used it as the data source, instead of the RSSChannel object. When you select a cell, it creates an instance of ParentAndRepliesViewController and passes it the thread array as its data source. (code snippet below) There are several other changes I’ve made to LVC and you can find the code here: https://bitbucket.org/mthongva/nfchallenge. I know it’s pretty sloppy so if anyone has any suggestions, I’d love to hear them.

ParentAndRepliesViewController.h

#import <Foundation/Foundation.h>
@class WebViewController;

@interface ParentAndRepliesViewController : UITableViewController

@property (nonatomic, strong) NSArray *parentAndReplies;
@property (nonatomic, strong) WebViewController *webViewController;

@end

ParentAndRepliesViewController.m

#import "ParentAndRepliesViewController.h"
#import "RSSItem.h"
#import "RSSChannel.h"
#import "WebViewController.h"

@implementation ParentAndRepliesViewController
@synthesize parentAndReplies, webViewController;

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

#pragma mark - tableView section

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [parentAndReplies count];
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"UITableViewCell"];
    
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"UITableViewCell"];
    }
    
    RSSItem *item = [parentAndReplies objectAtIndex:indexPath.row];
    
    [[cell textLabel] setText:[item title]];
    return cell;
    
}

-(void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath
{
    UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:webViewController];
    
    NSArray *vcontrollers = [NSArray arrayWithObjects:[self navigationController], nav, nil];
    [[self splitViewController] setViewControllers:vcontrollers];
    
    [[self splitViewController] setDelegate:webViewController];
    [self transferBarButtonToViewController:webViewController];
    
    RSSItem *entry = [parentAndReplies objectAtIndex:indexPath.row];
    
    [webViewController listViewController:nil handleObject:entry];
    
    [[tableView cellForRowAtIndexPath:indexPath] setHighlighted:NO];
}

-(void)transferBarButtonToViewController:(UIViewController *)vc
{
    // Get the navigation controller in the detail spot of the split view controller
    UINavigationController *nvc = [[[self splitViewController] viewControllers]
                                   objectAtIndex:1];
    
    // Get the root view controller out of that nav controller
    UIViewController *currentVC = [[nvc viewControllers] objectAtIndex:0];
    
    if (vc == currentVC)
        return;
    
    UINavigationItem *currentVCItem = [currentVC navigationItem];
    
    [[vc navigationItem] setLeftBarButtonItem:[currentVCItem leftBarButtonItem]];
    [currentVCItem setLeftBarButtonItem:nil];
}

@end

Here is the ListViewController’s new tableView:didSelectRowAtIndexPath method

-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    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:webViewController animated:YES];
    } else {
        // we have to create a new nav controller, as the old one
        // was only retained by teh split view controller and is now gone
        UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:webViewController];
        
        // create a new subclassed tableViewController to push on top of our
        // ListViewController
        ParentAndRepliesViewController *prvc = [[ParentAndRepliesViewController alloc] initWithStyle:UITableViewStylePlain];
        [prvc setParentAndReplies:[[replies replyCollection] objectAtIndex:indexPath.row]];
        [prvc setWebViewController:webViewController];
        
        // if the thread has several replies push the replies into view, if not
        // just load the post in the detail view of the split view.
        if ([[[replies replyCollection] objectAtIndex:indexPath.row] count] > 1) 
            [[self navigationController] pushViewController:prvc animated:YES];
        
        NSArray *vcs = [NSArray arrayWithObjects:[self navigationController], nav, nil];
        
        [[self splitViewController] setViewControllers:vcs];
        
        // make the detail view controller the delegate of the split view controller
        // - ignore this warning
        [[self splitViewController] setDelegate:webViewController];
        
        [self transferBarButtonToViewController:webViewController];
    }
    
    // grab the selected item
    // RSSItem *entry = [[channel items] objectAtIndex:[indexPath row]];
    RSSItem *entry = [[[replies replyCollection] objectAtIndex:indexPath.row] objectAtIndex:0];
    
    [webViewController listViewController:self handleObject:entry];
    
}

#10

I took similar approach as flipyrface.

For Model generation

  1. Added 2 new properties to RSSItem (RSSItem* parent, BOOL hasChild)
  2. Add a new properties to RSSChannel to hold so-called “parent post” (NSMutableArray* parentItems)
  3. Update these 3 new properties after performing trimTitles

For View generation

  1. Change the ListViewController table contents from items to parentItems
  2. Display Detail Disclosure Button on the cell only when hasChild = YES
  3. Add a new TableViewController, DetailListViewController, to show the child RSSItems for a selected parent RSSItem, and push it when the Disclosure Button is tapped.

Hope this will help.
Thanks.

RSSItem.h

@property (nonatomic, weak) RSSItem* parent; @property (nonatomic, assign)BOOL hasChild;

RSSChannel.h

@property (nonatomic, readonly, strong) NSMutableArray* parentItems; -(void)updateParentItems;

RSSChannel.m

-(void)updateParentItems{
	for (RSSItem* item in items) {
		//Assume the oldest post with same trimedTitle is the parent post
		NSPredicate* predicate = [NSPredicate predicateWithFormat:@"title = %@", [item title]];
		NSArray* sameTitledItems = [parentItems filteredArrayUsingPredicate:predicate];
		
		if ([sameTitledItems count] == 0) {
			[parentItems addObject:item];
			[item setParent:item]; //<-- pointing to itself in order to display the parent itself in the DetailView
			[item setHasChild:NO];
			
		} else {
			RSSItem* parentItem = [sameTitledItems objectAtIndex:0];
			[item setParent:parentItem];
			[parentItem setHasChild:YES];
			
		}
	}
}

-(void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName{
	
	currentString = nil;
	
	if ([elementName isEqual:@"channel"]) {
		[parser setDelegate:parentParserDelegate];
		[self trimItemTitles];
		[self updateParentItems];
	}
}

ListViewController.m

[code]#pragma mark TableView DataSource
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return [[channel parentItems] count];
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{

//alloc cell
NerdFeedItemCell* cell = [tableView dequeueReusableCellWithIdentifier:@"NerdFeedItemCell"];

//setup cell contents
RSSItem* item = [[channel parentItems] objectAtIndex:[indexPath row]];
[[cell title] setText:[item title]];
[[cell category] setText:[item category]];
[[cell auther] setText:[item author]];

if ([item hasChild]) {
	[cell setAccessoryType:UITableViewCellAccessoryDetailDisclosureButton];
} else {
	[cell setAccessoryType:UITableViewCellAccessoryNone]; //<-- Without this else clause, Disclosure Button will be displayed even when [item hasChild] is FALSE!!!
}

return cell;

}

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
return 66;
}

#pragma mark TableView Delegate
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
if (![self splitViewController]) {
[[self navigationController] pushViewController:webViewController animated:YES];

} else {
	[self transferBarButtonToViewController:webViewController];
	
	UINavigationController* nav = [[UINavigationController alloc] initWithRootViewController:webViewController];
	NSArray* vcs = [NSArray arrayWithObjects:[self navigationController], nav, nil];
	[[self splitViewController] setViewControllers:vcs];
	[[self splitViewController] setDelegate:webViewController];
}

RSSItem* entry = [[channel parentItems] objectAtIndex:[indexPath row]];

[webViewController listViewController:self handleObject:entry];

}

-(void)tableView:(UITableView )tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath )indexPath{
NSPredicate
predicate = [NSPredicate predicateWithFormat:@“parent == %@”, [[channel parentItems]objectAtIndex:[indexPath row]]];
NSArray
children = [[channel items] filteredArrayUsingPredicate:predicate];

DetailListViewController* detailListVC = [[DetailListViewController alloc] initWithItems];
[detailListVC setWebViewController:webViewController];
[[self navigationController] pushViewController:detailListVC animated:YES];

}
[/code]

DetailListViewController.h

[code]#import <Foundation/Foundation.h>
@class RSSItem;
@class WebViewController;

@interface DetailListViewController : UITableViewController<UITableViewDataSource, UITableViewDelegate>

@property (nonatomic, strong) NSArray* childItems;
@property (nonatomic, strong) WebViewController* webViewController;

-(id)initWithItems:(NSArray*)items;

@end[/code]

DetailListViewController.m

[code]#import “DetailListViewController.h”
#import “RSSItem.h”
#import “WebViewController.h”
#import “NerdFeedItemCell.h”
#import “ListViewController.h”

@implementation DetailListViewController

@synthesize childItems;
@synthesize webViewController;

-(id)initWithItems:(NSArray *)items{
self = [super init];
if (self) {
childItems = [[NSArray alloc] initWithArray:items];
}
return self;
}

-(void)viewDidLoad{
[super viewDidLoad];
UINib* nib = [UINib nibWithNibName:@“NerdFeedItemCell” bundle:nil];
[[self tableView] registerNib:nib forCellReuseIdentifier:@“NerdFeedItemCell”];
}

#pragma TableView DataSource
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return [childItems count];
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{

NerdFeedItemCell* cell = [tableView dequeueReusableCellWithIdentifier:@"NerdFeedItemCell"];

RSSItem* item = [childItems objectAtIndex:[indexPath row]];
[[cell title] setText:[item title]];
[[cell category] setText:[item category]];
[[cell auther] setText:[item author]];

return cell;

}

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
return 66;
}

#pragma mark TableView Delegate
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
if (![self splitViewController]) {
[[self navigationController] pushViewController:webViewController animated:YES];

} else {
	ListViewController* lvc = [[[self navigationController] viewControllers] objectAtIndex:0];
	[lvc transferBarButtonToViewController:webViewController];
	
	UINavigationController* nav = [[UINavigationController alloc] initWithRootViewController:webViewController];
	NSArray* vcs = [NSArray arrayWithObjects:[self navigationController], nav, nil];
	[[self splitViewController] setViewControllers:vcs];
	[[self splitViewController] setDelegate:webViewController];
}

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

[webViewController listViewController:self handleObject:entry];

}

@end[/code]