Bronze / Silver / Gold challenges solution


#1

Hi all,

Here’s my solution that satisfies the requirements for all three challenges.

JJItemsViewController.m

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

@interface JJItemsViewController ()

@property (strong, nonatomic)  IBOutlet UIView *headerView;

@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) {

    }
    return self;
}

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

-(void) viewDidLoad
{
    [super viewDidLoad];
    
    [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"UITableViewCell"];
    
    UIView *header = self.headerView;
    [self.tableView setTableHeaderView:header];
}



#pragma mark - UITableView Datasource

-(NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [[[JJItemStore sharedStore] allItems] count] + 1;
}

-(UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"UITableViewCell" forIndexPath:indexPath];
    
    if (indexPath.row == [[[JJItemStore sharedStore] allItems] count]) {
        cell.textLabel.text = @"No more items!";
    } else {
        JJItem *item = [[JJItemStore sharedStore] allItems][indexPath.row];
        cell.textLabel.text = [item description];
    }
    return cell;
}

-(void)      tableView:(UITableView *)tableView
    commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
     forRowAtIndexPath:(NSIndexPath *)indexPath
{
    // If the table view is asking to commit a delete command
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        NSArray *allItems = [[JJItemStore sharedStore] allItems];
        JJItem *item = allItems[indexPath.row];
        [[JJItemStore sharedStore] removeItem:item];
        
        // Also remove that row from the table view with an animation
        [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
    }
}

-(void)      tableView:(UITableView *)tableView
    moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath
           toIndexPath:(NSIndexPath *)destinationIndexPath
{
    [[JJItemStore sharedStore] moveItemAtIndex:sourceIndexPath.row toIndex:destinationIndexPath.row];
    [tableView reloadData];

}

-(NSString *)                               tableView:(UITableView *)tableView
    titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return @"Remove";
}

-(BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (indexPath.row == [[[JJItemStore sharedStore] allItems] count]) {
        return NO;
    } else {
        return YES;
    }
}



#pragma mark - Customized Views

-(UIView *) headerView
{
    if (!_headerView) {
        [[NSBundle mainBundle] loadNibNamed:@"HeaderView" owner:self options:nil];
    }
    return _headerView;
}



#pragma mark - IBActions

-(IBAction) addNewItem:(id)sender
{
    JJItem *newItem = [[JJItemStore sharedStore] createItem];
    NSInteger lastRow = [[[JJItemStore sharedStore] allItems] indexOfObject:newItem];
    NSIndexPath *indexPath = [NSIndexPath indexPathForItem:lastRow inSection:0];
    
    [self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationTop];
}

-(IBAction) toggleEditingMode:(id)sender
{
    // If already in edit mode
    if (self.isEditing) {
        
        // Change title of button to inform user of state and turn off editing mode
        [sender setTitle:@"Edit" forState:UIControlStateNormal];
        [self setEditing:NO animated:YES];
        
    } else {
        
        // Change title of button to inform user of state and turn on editing mode
        [sender setTitle:@"Done" forState:UIControlStateNormal];
        [self setEditing:YES animated:YES];
    }
}

@end

Bronze: Renamed the “Delete” confirmation button by implementing the tableView:titleForDeleteConfirmationButtonForRowAtIndexPath: method.

Silver: In the tableView:numberOfRowsInSection: method, returned the count of the JJItemStore allItems array + 1, so that there would always be a last row present. Then, in the tableView:cellForRowAtIndexPath: method, checked to see if indexPath.row was equal to the count of JJItemStore’s allItems array. If so, set the cell’s text to “No more items!”. To prevent this row from being moved, implemented some logic in the tableView:canEditRowAtIndexPath method that checks whether or not indexPath.row was equal to the count of JJItemStore’s allItems array. If it is, it’s the “No more items!” row, and the method returns NO. If not, the method returns YES.

JJItemStore.m (Gold challenge solution)

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

@interface JJItemStore ()

@property (nonatomic) NSMutableArray *privateItems;

@end

@implementation JJItemStore

+(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];
    
    [self.privateItems addObject:item];
    
    return item;
}

-(void) removeItem:(JJItem *)item
{
    [self.privateItems removeObjectIdenticalTo:item];
}

-(void) moveItemAtIndex:(NSUInteger)fromIndex toIndex:(NSUInteger)toIndex
{
    // If the toIndex is the same as the fromIndex or if the toIndex is below the final tableViewCell ("No more items!")
    if (fromIndex == toIndex || toIndex == [[[JJItemStore sharedStore] allItems] count]) {
        
        return;
        
    } else {
        
        // Get pointer to object being moved so it can be reinserted
        JJItem *item = self.privateItems[fromIndex];
        
        // Remove item from the array
        [self.privateItems removeObjectAtIndex:fromIndex];
        
        // Insert item in array at new location
        [self.privateItems insertObject:item atIndex:toIndex];
        
    }
}

@end

Gold: Left this one up to the data model rather than the view controller. If the toIndex being passed equals the count of JJItemStore’s allItems array, the method returns and does not finish executing. Then, in the tableView:moveRowAtIndexPath:toIndexPath method in JJItemsViewController, call [tableView reloadData] which returns the cell visually back to where it came from.


#2

You shouldn’t change the model (JJItemStore moveItemAtIndex) to accommodate the view.


#3

Very similar to my silver solution, but there is a problem with this. You can move an item to the last position, hit done, hit edit, and remove “No more items!” still trying to get this worked out to prevent the last row from being replaced/moved.

Edit: I finally found out how to prevent other rows to be moved into the last row. Use tableView:targetIndexPathForMoveFromRowAtIndexPath:toProposedIndexPath: :slight_smile:

Edit again: I got ahead of myself while working on the Silver challenge because it irritated me that I could still move the last object and delete it. Using tableView:targetIndexPathForMoveFromRowAtIndexPath:toProposedIndexPath: is actually part of the gold challenge lol.