Solution that satisfies all three challenge requirements


#1

Hi all,

So with a little help from you fine folks I was able to work up a solution that’s highly flexible and will work with 0 items, only 1 item, or multiple items. It handles grouping items into >$50 and <$50, ALWAYS appends a “No more items!” row to the last row of only the last section, even if there are no items. It also satisfies the Gold Challenge’s customization requirements.

I included a UINavigationBar in the app delegate because the way the TableView clashed with the status bar drove me nuts.

JJAppDelegate.m - application:didFinishLaunchingWithOptions: method

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    
    JJItemsViewController *itemsViewController = [[JJItemsViewController alloc] init];
    UINavigationController *navigationVC = [[UINavigationController alloc] initWithRootViewController:itemsViewController];
    itemsViewController.title = @"Homepwner";
    
    self.window.rootViewController = navigationVC;
    
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    return YES;
}

JJItemStore.m

#import "JJItemStore.h"
#import "JJItem.h"

@interface JJItemStore ()

@property (nonatomic) NSMutableArray *privateItems;

@property (strong, nonatomic) NSMutableArray *itemsOver50;
@property (strong, nonatomic) NSMutableArray *itemsUnder50;

@end

@implementation JJItemStore

-(NSMutableArray *) itemsOver50
{
    if (!_itemsOver50) {
        _itemsOver50 = [[NSMutableArray alloc] init];
    }
    return _itemsOver50;
}

-(NSMutableArray *) itemsUnder50
{
    if (!_itemsUnder50) {
        _itemsUnder50 = [[NSMutableArray alloc] init];
    }
    return _itemsUnder50;
}

+(instancetype) sharedStore
{
    static JJItemStore *sharedStore = nil;
    
    if (!sharedStore) {
        sharedStore = [[self alloc] initPrivate];
    }
    return sharedStore;
}

-(instancetype) init
{
    @throw [NSException exceptionWithName:@"Singleton"
                                   reason:@"Use +[JJItemStore sharedStore]"
                                 userInfo:nil];
    return nil;
}

-(instancetype) initPrivate
{
    self = [super init];
    if (self) {
        _privateItems = [[NSMutableArray alloc] init];
    }
    return self;
}

-(NSArray *) allItems
{
    return self.privateItems;
}

-(JJItem *) createItem
{
    JJItem *item = [JJItem randomItem];
    
    if (item.valueInDollars > 50) {
        
        if ([self.itemsOver50 count] == 0) { // If the itemsOver50 array currently has no items inside, add it to the privateItems array
            [self.privateItems addObject:self.itemsOver50];
        }
        
        [self.itemsOver50 addObject:item];
        
    } else {
        
        if ([self.itemsUnder50 count] == 0) { // If the itemsUnder50 array currently has no items inside, add it to the privateItems array
            [self.privateItems addObject:self.itemsUnder50];
        }
        
        [self.itemsUnder50 addObject:item];
        
    }
    
    return item;
}

@end

JJItemsViewController.m

#import "JJItemsViewController.h"
#import "JJItemStore.h"
#import "JJItem.h"

@interface JJItemsViewController ()

@property (strong, nonatomic) UITableView *backgroundView;

@end

@implementation JJItemsViewController

// Overriding init and the designated initializer will ensure all instances of this VC use the UITableViewStylePlain style
-(instancetype) init
{
    self = [super initWithStyle:UITableViewStylePlain];
    if (self) {
        for (int i = 0 ; i < 5 ; i++) {
            [[JJItemStore sharedStore] createItem];
        }
    }
    return self;
}

-(instancetype) initWithStyle:(UITableViewStyle)style
{
    return [self init];
}

-(void) viewDidLoad
{
    [super viewDidLoad];
    
    UIImage *backgroundImage = [UIImage imageNamed:@"bgImage.png"];
    UIView *backgroundView = [[UIView alloc] initWithFrame:self.view.bounds];
    [backgroundView setBackgroundColor:[UIColor colorWithPatternImage:backgroundImage]];
    
    self.tableView.backgroundColor = [UIColor clearColor];
    self.tableView.backgroundView = backgroundView;
    
    [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"UITableViewCell"];
    
    NSLog(@"%i", [[[JJItemStore sharedStore] allItems] count]);
}



#pragma mark - Headers

-(CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
    return 60;
}

-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
    UIView *headerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, tableView.bounds.size.width, 60)];
    
    headerView.backgroundColor = [UIColor whiteColor];
    
    CGPoint center = headerView.center;
    
    UILabel *titleLabel = [[UILabel alloc] initWithFrame:headerView.frame];
    titleLabel.center = center;
    [titleLabel setTextAlignment:NSTextAlignmentCenter];
    titleLabel.font = [UIFont fontWithName:@"Avenir Next" size:32];
    
    if ([tableView numberOfSections] > 1) {
        
        if (section == 0) {
            
            titleLabel.text = @"> $50";
            
        } else
            
            titleLabel.text = @"< $50";
        
    } else { // There is only one array inside the allItems array.
        
        titleLabel.text = @"Items";
        
    }
    
    [headerView addSubview:titleLabel];
    
    return headerView;
}



#pragma mark - UITableView Datasource

-(NSInteger) numberOfSectionsInTableView:(UITableView *)tableView
{
    NSArray *allItems = [[JJItemStore sharedStore] allItems];
    
    if ([allItems count] == 0) { // The allItems array is empty.
        
        return 1; // Need one section for the "No more items!" row.
        
    } else {
        
        return [allItems count];
        
    }
}

-(NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    NSArray *allItems = [[JJItemStore sharedStore] allItems];
    
    if ([allItems count] == 0) { // There are no items inside the allItems array and only one row is needed for the "No more items!" cell.
        
        return 1;
        
    }
    
    NSArray *itemsInSection = allItems[section]; // Grab the array that's stored in the allItems array at the index relative to the current section.
    NSInteger sum;
    
    if (section == [tableView numberOfSections] - 1) { // This is the last section. Add a row to account for the "No more items!" cell.
        
        sum = [itemsInSection count] + 1;
        
    } else {
        
        sum = [itemsInSection count];
        
    }
    
    return sum;
}

-(UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"UITableViewCell" forIndexPath:indexPath];
    
    NSArray *allItems = [[JJItemStore sharedStore] allItems];
    
    NSInteger sum; // Declared outside of any "if" blocks so it can be accessed in multiple blocks
    
    if ([allItems count] == 0) { // The allItems array is empty. Create one cell for "No more items!"
        
        cell.textLabel.text = @"No more items!";
        cell.textLabel.font = [UIFont fontWithName:@"Avenir Next" size:[UIFont systemFontSize]];
        return cell;
        
    } else {
        
        sum = [allItems[indexPath.section] count];
        
    }
    
    if (indexPath.section == [tableView numberOfSections] - 1 && indexPath.row == sum) { // This is the last row in the last section. Create the "No more items!" cell.
        
        cell.textLabel.text = @"No more items!";
        cell.textLabel.font = [UIFont fontWithName:@"Avenir Next" size:[UIFont systemFontSize]];
        
    } else {
        
        JJItem *item = allItems[indexPath.section][indexPath.row];
        cell.textLabel.text = [item description];
        cell.textLabel.font = [UIFont fontWithName:@"Avenir Next" size:20];
    }
    
    cell.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.25];
    return cell;
}

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    CGFloat height = 60;
    NSArray *allItems = [[JJItemStore sharedStore] allItems];
    NSInteger sum = 0;
    
    
    // If there are no items in the allItems array or the indexPath is the last row
    if ([allItems count] == 0 || (indexPath.section == [tableView numberOfSections] - 1 && indexPath.row == sum)) {
        height = [tableView rowHeight];
    }
    
    return height;
    
}

@end

Hope this helps some people out!


#2

Wow, this is just gorgeous.
Using a 2-dimensional array for the two sets of items was a pretty darned good call!


#3

[quote=“Psyrixx”]Wow, this is just gorgeous.
Using a 2-dimensional array for the two sets of items was a pretty darned good call![/quote]

Thanks! Glad you like it!


#4

This is, by far, the most elegant solution to the bronze challenge that I’ve seen yet. It was extremely helpful. Well done!

I thought about using an NSDictionary to sort the items by value and associating each group with a specific key, but I felt that would have been beyond the scope of the challenge, given that NSDictionary has yet to be covered in the book. Two ways of doing the exact same thing - tomato, to-mah-to.

Tip of the cap to you, good sir.


#5

I like your use of the navigation bar. You could also adjust the content size by including the statement –

self.tableView.contentInset = UIEdgeInsetsMake(32, 0, 0, 0);

in viewDidLoad.


#6

[quote=“hpdotnet”]This is, by far, the most elegant solution to the bronze challenge that I’ve seen yet. It was extremely helpful. Well done!

I thought about using an NSDictionary to sort the items by value and associating each group with a specific key, but I felt that would have been beyond the scope of the challenge, given that NSDictionary has yet to be covered in the book. Two ways of doing the exact same thing - tomato, to-mah-to.

Tip of the cap to you, good sir.[/quote]

NSDictionary is one of my favorite classes and I try to use it as much as I can, so I definitely understand where you’re coming from here. You’re right - I tried to stick to exactly where we were at in the book, even though using a dictionary would have made this SO much easier. Part of learning to appreciate the language, I guess! Thanks so much for the kind words, I’m glad you like the solution. How’d yours come out?


#7

[quote=“stubess”]I like your use of the navigation bar. You could also adjust the content size by including the statement –

self.tableView.contentInset = UIEdgeInsetsMake(32, 0, 0, 0);

in viewDidLoad.[/quote]

Works great! Thanks for the tip.


#8

Mine turned out great … finally. It felt like climbing a freaking mountain. :slight_smile: I wasn’t too concerned with the aesthetic. Overall, I took a very similar approach and kept the sorting work in the model class. I ended up adding the sub-arrays to the parent array on initialization.

BNRItemsViewController.m

#import "BNRItemsViewController.h"
#import "BNRItemStore.h"
#import "BNRItem.h"

@implementation BNRItemsViewController

-(instancetype)init {
    // Call the superclass's designated initializer
    self = [super initWithStyle:UITableViewStylePlain];
    if (self) {
        for (int i = 0; i < 5; i++) {
           [[BNRItemStore sharedStore] createItem];
        }
    }
    return self;
}

-(instancetype)initWithStyle:(UITableViewStyle)style {
    return [self init];
}

-(void)viewDidLoad {
    [super viewDidLoad];
    
    [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"UITableViewCell"];
}

-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return [[[BNRItemStore sharedStore] allItems] count];
}

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    NSArray *allItems = [[BNRItemStore sharedStore] allItems];
    NSInteger lastSectionIndex = [tableView numberOfSections] - 1;
    if (section == lastSectionIndex) {
        return [allItems[section] count] + 1;
    } else {
        return [allItems[section] count];
    }
}

-(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
    NSArray *allItems = [[BNRItemStore sharedStore] allItems];
    if ([allItems[section] count] > 0) {
        if (section == 0) {
            return @"Items Over $50";
        } else {
            return @"Items Under $50";
        }
    } else {
        return nil;
    }
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    // Get a new or recycled cell
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"UITableViewCell" forIndexPath:indexPath];
    
    // Set the text on the cell with the description of the item
    // that is at the nth index of items, where n = row this cell
    // will appear in on the tableview
    NSArray *allItems = [[BNRItemStore sharedStore] allItems];
    
    NSInteger lastSectionIndex = [tableView numberOfSections] - 1;
    NSInteger lastRowIndex = [tableView numberOfRowsInSection:lastSectionIndex] - 1;
    
    if ((indexPath.section == lastSectionIndex) && (indexPath.row == lastRowIndex)) {
        cell.textLabel.text = @"No more items!";
    } else {
        BNRItem *item = allItems[indexPath.section][indexPath.row];
        cell.textLabel.text = [item description];
    }
    
    return cell;
}

@end

BNRItemsStore.m

#import "BNRItemStore.h"
#import "BNRItem.h"

@interface BNRItemStore ()

@property (nonatomic) NSMutableArray *privateItems;
@property (nonatomic) NSMutableArray *under50;
@property (nonatomic) NSMutableArray *over50;

@end

@implementation BNRItemStore

+(instancetype)sharedStore {
    static BNRItemStore *sharedStore = nil;
    
    // Do I need to create a sharedStore?
    if (!sharedStore) {
        sharedStore = [[self alloc] initPrivate];
    }
    
    return sharedStore;
}

// If a programmer calls [[BNRItemsStore alloc] init], let him
// know the error of his ways
-(instancetype)init {
    @throw [NSException exceptionWithName:@"Singleton" reason:@"Use +[BNRItemStore sharedStore]" userInfo:nil];
    return nil;
}

// Here is the real initializer
-(instancetype)initPrivate {
    self = [super init];
    if (self) {
        _privateItems = [[NSMutableArray alloc] init];
        _under50 = [[NSMutableArray alloc] init];
        _over50 = [[NSMutableArray alloc] init];
        
        [_privateItems addObject:_over50];
        [_privateItems addObject];
    }
    return self;
}

-(NSArray *)allItems {
    return self.privateItems;
}

-(BNRItem *)createItem {
    BNRItem *item = [BNRItem randomItem];
    
    if (item.valueInDollars > 50) {
        [self.over50 addObject:item];
    } else {
        [self.under50 addObject:item];
    }
    
    return item;
}

@end

#9

Mine turned out great … finally. It felt like climbing a freaking mountain. :slight_smile: I wasn’t too concerned with the aesthetic. Overall, I took a very similar approach and kept the sorting work in the model class. I ended up adding the sub-arrays to the parent array on initialization.

BNRItemsViewController.m

#import "BNRItemsViewController.h"
#import "BNRItemStore.h"
#import "BNRItem.h"

@implementation BNRItemsViewController

-(instancetype)init {
    // Call the superclass's designated initializer
    self = [super initWithStyle:UITableViewStylePlain];
    if (self) {
        for (int i = 0; i < 5; i++) {
           [[BNRItemStore sharedStore] createItem];
        }
    }
    return self;
}

-(instancetype)initWithStyle:(UITableViewStyle)style {
    return [self init];
}

-(void)viewDidLoad {
    [super viewDidLoad];
    
    [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"UITableViewCell"];
}

-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return [[[BNRItemStore sharedStore] allItems] count];
}

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    NSArray *allItems = [[BNRItemStore sharedStore] allItems];
    NSInteger lastSectionIndex = [tableView numberOfSections] - 1;
    if (section == lastSectionIndex) {
        return [allItems[section] count] + 1;
    } else {
        return [allItems[section] count];
    }
}

-(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
    NSArray *allItems = [[BNRItemStore sharedStore] allItems];
    if ([allItems[section] count] > 0) {
        if (section == 0) {
            return @"Items Over $50";
        } else {
            return @"Items Under $50";
        }
    } else {
        return nil;
    }
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    // Get a new or recycled cell
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"UITableViewCell" forIndexPath:indexPath];
    
    // Set the text on the cell with the description of the item
    // that is at the nth index of items, where n = row this cell
    // will appear in on the tableview
    NSArray *allItems = [[BNRItemStore sharedStore] allItems];
    
    NSInteger lastSectionIndex = [tableView numberOfSections] - 1;
    NSInteger lastRowIndex = [tableView numberOfRowsInSection:lastSectionIndex] - 1;
    
    if ((indexPath.section == lastSectionIndex) && (indexPath.row == lastRowIndex)) {
        cell.textLabel.text = @"No more items!";
    } else {
        BNRItem *item = allItems[indexPath.section][indexPath.row];
        cell.textLabel.text = [item description];
    }
    
    return cell;
}

@end

BNRItemsStore.m

[code]
#import “BNRItemStore.h”
#import “BNRItem.h”

@interface BNRItemStore ()

@property (nonatomic) NSMutableArray *privateItems;
@property (nonatomic) NSMutableArray *under50;
@property (nonatomic) NSMutableArray *over50;

@end

@implementation BNRItemStore

+(instancetype)sharedStore {
static BNRItemStore *sharedStore = nil;

// Do I need to create a sharedStore?
if (!sharedStore) {
    sharedStore = [[self alloc] initPrivate];
}

return sharedStore;

}

// If a programmer calls [[BNRItemsStore alloc] init], let him
// know the error of his ways
-(instancetype)init {
@throw [NSException exceptionWithName:@“Singleton” reason:@“Use +[BNRItemStore sharedStore]” userInfo:nil];
return nil;
}

// Here is the real initializer
-(instancetype)initPrivate {
self = [super init];
if (self) {
_privateItems = [[NSMutableArray alloc] init];
_under50 = [[NSMutableArray alloc] init];
_over50 = [[NSMutableArray alloc] init];

    [_privateItems addObject:_over50];
    [_privateItems addObject];
}
return self;

}

-(NSArray *)allItems {
return self.privateItems;
}

-(BNRItem *)createItem {
BNRItem *item = [BNRItem randomItem];

if (item.valueInDollars > 50) {
    [self.over50 addObject:item];
} else {
    [self.under50 addObject:item];
}

return item;

}

@end
[/code][/quote]

Great work!