My Final Sections Challenge solution. Please give feedback


#1

Okay so here is my solution for the sections challenge once I got the bugs worked out. My goal here was to provide a flexible way to sort the possessions, and have the tableView adjust accordingly to the number of sections needed automatically. Most of the magic is in ItemsViewController, but there is a new method added to the PossessionStore that returns a sub array from all possessions based on any valid NSPredicate format string. It seams to be working well with no bugs or memory leaks that I have found. :unamused: The divideSections method does the grunt work in sorting out the possessions. It gets used in the init method and again anytime a method edits a possession’s details, deletes or adds a possession, or if the table is reordered by the user. I’ve tried to make as many of the tableView methods as ambiguous as possible so that if you decided to change the criteria you divide the sections by, you would only need to mod the divideSections function. As long as you store the sections in the array as NSDictionaries with a Possessions key to an array of possessions, and a Header key for the section title the rest should continue to work with no or little modification. This code does generate three warnings that I haven’t been able to work around yet, but runs fine. I would love to hear suggestions on ways to fix that and still preserve my desired goal. There are comments to indicate where and what the warnings are in tableView:commitEditingStyle:forRowAtIndexPath:, tableView:moveRowAtIndexPath:toIndexPath: and in tableView:numberOfRowsInSection:. Please feel free to use this, offer feedback or suggest improvements. :smiley:

PossessionStore

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

@class Possession;
@interface PossessionStore : NSObject
{
NSMutableArray *allPossessions;
NSArray *stuff; // this is my new array for returning subsets of the data
}

  • (PossessionStore *)defaultStore;

-(NSArray *)possesionsFromPredicate:(NSString *)predicateString; // this function determines if a subset of data or all possessions get returned
// … the rest of the header missing for brevity …
@end[/code]
possessionsFromPredicate method in PossessionStore.m

-(NSArray *)possesionsFromPredicate:(NSString *)predicateString
{
	//NSLog(@"Returning stuff from predicate:%@", predicateString);
	if (predicateString == @"all") {
		return allPossessions;
	}
	NSPredicate *predicate = [NSPredicate predicateWithFormat:predicateString];
	stuff = [[self allPossessions] filteredArrayUsingPredicate:predicate];
	return stuff;
}

ItemsViewController

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

@interface ItemsViewController : UITableViewController
{
NSMutableArray *myTableSections; // variable to hold the number of sections
}
-(void)addNewPossession:(id)sender;
-(void)divideSections;
@end[/code]

//  ItemsViewController.m
#import "ItemsViewController.h"
#import "PossessionStore.h"
#import "ImageStore.h"
#import "Possession.h"
#import "HomepwnerItemCell.h"

@implementation ItemsViewController

- (id) init
{
	//NSLog(@"ItemsViewController init called");
	// Call the superclass's designated initializer
	self = [super initWithStyle:UITableViewStyleGrouped];
	
	if (self) {
		// create a new barItem that will send addNePossession: to itemsViewController
		UIBarButtonItem *bbi = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addNewPossession:)];
		
		// Set this barButtonItem as the right item in the Navigation item
		[[self navigationItem] setRightBarButtonItem:bbi];
		
		//The Navigation item retains its buttons so release bbi
		[bbi release];
		
		// set the title of the navigation item
		[[self navigationItem] setTitle:@"Homepwner"];
		
		[[self navigationItem] setLeftBarButtonItem:[self editButtonItem]];
		myTableSections = [[[NSMutableArray alloc] init] retain];
		
		// set up sections here by dividing allPossessions
		[self divideSections];
	}
	return self;
}

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

-(void)divideSections
{
	//NSLog(@"divideSections called");

        // This is the method to configure how your table is sorted. Create sub arrays and store them in NSDictionaries as shown below and then place the dictionary in the mySectionsArray
	// For simplicity just empty out the sections array and rebuild it each time we need to divide the sections out.
	[myTableSections removeAllObjects];

        // you can default to 1 section with all possessions in it by using the predicate string @"all" -- this is the only predefined predicate the method aside form that
	// any valid and relevant string conforming to valid NSPredicate could be used here depending on how you want sections split.
	NSArray *cheapStuff = [[NSArray alloc] initWithArray:[[PossessionStore defaultStore] possesionsFromPredicate:@"valueInDollars < 50"]];
	NSArray *expensiveStuff = [[NSArray alloc] initWithArray:[[PossessionStore defaultStore] possesionsFromPredicate:@"valueInDollars >= 50"]];

	// make an NSDictionary for each section. it will hold an array of possesions for each section and another key will serve as the sections header. add the dictionary to myTableSections.
	if ([cheapStuff count] > 0) { 
		NSMutableDictionary *section1 = [NSMutableDictionary dictionaryWithObject:cheapStuff forKey:@"Possessions"];
		[section1 setValue:@"Cheap Stuff" forKey:@"Header"];
		[myTableSections addObject];
	}
	if ([expensiveStuff count] > 0) {
		NSMutableDictionary *section2 = [NSMutableDictionary dictionaryWithObject:expensiveStuff forKey:@"Possessions"];
		[section2 setValue:@"Expensive Stuff" forKey:@"Header"];
		[myTableSections addObject];
	}
	//now our arrays are retained by the dictionarys so we release them
	[cheapStuff release];
	[expensiveStuff release];
	// NSLog(@" End of divideSections myTableSections is holding - %@", myTableSections);
}

 -(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
	ItemDetailViewController *detailViewController = [[[ItemDetailViewController alloc] initForNewItem:NO] autorelease];
	// give the detail view controller a pointer to the possesion object in row
	// get the NSDictionary located at the section index, get the dictionary's array, get the possession at row index	
	Possession *p = [[[myTableSections objectAtIndex:[indexPath section]] objectForKey:@"Possessions"] objectAtIndex:[indexPath row]];
	[detailViewController setPossession:p];
	
	// push it onto the navigationControllers stack
	[[self navigationController] pushViewController:detailViewController animated:YES];	
}

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{		////If the table view is asking to commit the delete command
	if (editingStyle == UITableViewCellEditingStyleDelete) {
		BOOL lastRow = FALSE;
		if ([tableView numberOfRowsInSection:[indexPath section]] == 1 ) {
			lastRow = TRUE;
		}		
		// Get the possesion from the row
		Possession *p = [[[myTableSections objectAtIndex:[indexPath section]] objectForKey:@"Possessions"] objectAtIndex:[indexPath row]];
		
		// If we have an image in the store for possession delete it
		// Get the image key
		NSString *theImageKey = [p imageKey];
		
		if (theImageKey) {
			// delete the image from the store
			[[ImageStore defaultImageStore] deleteImageForKey:theImageKey];
		}
		
		// delete possession from the PossessionStore
///////////////////////// Warning from the following line - 'NSArray' may not respond to 'removeObjectIdenticalTo:' ///////////////////////////////////////
		[[[PossessionStore defaultStore] allPossessions] removeObjectIdenticalTo:p];
		
		// Rebuild myTableViewSections to reflect the data change to the PossessionStore
		[self divideSections];
		
			// remove the row or section if last row from the table view with an animation
		if (lastRow) {
			[tableView deleteSections:[NSIndexSet indexSetWithIndex:indexPath.section] withRowAnimation:UITableViewRowAnimationFade];
		} else {
			[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:YES];
		}
	}
}

-(void) tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath
{
	// see if we have more that 1 section
	if ([tableView numberOfSections] > 1 ) {
		// rebuild the allPossesion array so it matches the current order of the 2 sections

////////////////////Warning from the following line -- Incompatible pointer types initializing 'NSMutableArray *' with an expression of type 'NSArray *'
		NSMutableArray *theStore = [[PossessionStore defaultStore] allPossessions];
		NSArray *section1 = [[myTableSections objectAtIndex:0] objectForKey:@"Possessions"];
		NSArray *section2 = [[myTableSections objectAtIndex:1] objectForKey:@"Possessions"];
		[theStore removeAllObjects];
		[theStore addObjectsFromArray];
		[theStore addObjectsFromArray];
	}
	// move the possesions in the default store
	[[PossessionStore defaultStore] movePossessionAtIndex:[sourceIndexPath row] toIndex:[destinationIndexPath row]];
	// rebuild sections to reflect changes
	[self divideSections];
}

-(void)addNewPossession:(id)sender
{
	ItemDetailViewController *detailViewController = [[ItemDetailViewController alloc] initForNewItem:YES];
	
	Possession *newPossession =[[PossessionStore defaultStore] createPossession];
	[detailViewController setPossession:newPossession];
	
	UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:detailViewController];
	
	[detailViewController setDelegate:self];
	
	[detailViewController release];
	
	[navController setModalPresentationStyle:UIModalPresentationFormSheet];
	[navController setModalTransitionStyle:UIModalTransitionStyleFlipHorizontal];
	
	[self presentModalViewController:navController animated:YES];
	
	// navController is retained by self  when presented
	[navController release];
}

-(void)itemDetailViewControllerWillDismiss:(ItemDetailViewController *)vc
{
	//we may have added a new possession so we'll divide out the sections again and reload the table data
	[self divideSections];
	[[self tableView] reloadData];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
	// NSLog(@"tableView:numberOfRowsInSection called for section %i, returning - %i",section, [[(NSArray *)[myTableSections objectAtIndex:section] objectForKey:@"Possessions"] count]);	

//////////////////////Warning from this return statement -- 'NSArray' may not respond to 'objectForKey:' -- the typecasting was done so that the count method worked without it my return was always null;
	return [[(NSArray *)[myTableSections objectAtIndex:section] objectForKey:@"Possessions"] count];
}

- (NSInteger) numberOfSectionsInTableView:(UITableView *)tableView
{
	// NSLog(@"returning number of sections: %i", [myTableSections count]);
		return [myTableSections count];
}

- (NSString *) tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
	// NSLog(@"tableView:titleForHeaderInSectionCalled - section = %i", section);
	//Configure the header titles based on the number of sections
	if ([myTableSections count] <= 1) {
		// return simple title for only one section in table
		return @"My Stuff";
	} else {
		// or return the key for the dictionary entry for the current section
		return [[myTableSections objectAtIndex:section] objectForKey:@"Header"];
	}
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	// NSLog(@"tableView:cellForRowAtIndexPath called for section %d, Row %d", [indexPath section], [indexPath row]);
	
	// Get an instance of a HomepwnerItemCell - either an unused one or a new one
	// the method returns a UITableviewCell; we typecast it as a HomePwnerItemCell.
	
	HomepwnerItemCell *cell = (HomepwnerItemCell *)[tableView dequeueReusableCellWithIdentifier:@"HomepwnerItemCell"];
	// If there is no cell of this type create a new one
	if (!cell) {
		cell = [[[HomepwnerItemCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"HomepwnerItemCell"] autorelease];
	}
	// get the NSDictionary located at the section index, get the dictionary's array, get the possession at row index	
	Possession *p = [[[myTableSections objectAtIndex:[indexPath section]] objectForKey:@"Possessions"] objectAtIndex:[indexPath row]];

	[cell setPossession:p];
	return cell;
}

-(void)viewWillAppear:(BOOL)animated
{
	[super viewWillAppear:YES];
	// divide sections again incase a possession was added or edited
	[self divideSections];
	[[self tableView] reloadData];
	//NSLog(@"viewWillAppear called");
}

-(void)viewDidUnload
{
	[myTableSections release];
	[super viewDidUnload];
}

-(void)viewDidLoad
{
	[super viewDidLoad];
	UIColor *clr = nil;
	if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
		clr = [UIColor colorWithRed:0.875 green:0.88 blue:0.91 alpha:1];
	} else {
		clr = [UIColor groupTableViewBackgroundColor];
	}
	
	[[self view] setBackgroundColor:clr];
	[self divideSections];
}

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

-(void)dealloc
{
	[myTableSections release];
	myTableSections = nil;
	[super dealloc];
}
@end

#2

Very cool, I actually did something similar with my PossessionStore. I’ve put the code below so you (and anyone else) can check it out

First, PossessionStore.h

@interface PossessionStore : NSObject
{
    NSMutableArray *allPossessions;
}

+ (PossessionStore *)defaultStore;

- (NSArray *)allPossessions;
- (Possession *)createPossession;
- (NSArray *)possessionsWithValueLessThanOrEqualTo:(NSNumber *)value;
- (NSArray *)possessionsWithValueGreaterThan:(NSNumber *)value;
- (NSArray *)possessionsMatchingPredicate:(NSPredicate *)predicate;

I didn’t use the extra stuff array that you did to store the variables, preferring to simply return them when the predicate filter was executed. Your method is probably more efficient in this case. I also added two convenience methods for searching specifically for possessions with values greater than/less than or equal to the provided value. These basically just call the possessionsMatchingPredicate method with their passed value like so:

- (NSArray *)possessionsMatchingPredicate:(NSPredicate *)predicate
{
    //todo CSM make sure this is correct, or if we can just return it and not use autorelease
    NSArray *matches = [[[self allPossessions] filteredArrayUsingPredicate:predicate] autorelease];
    
    return matches;
}

- (NSArray *)possessionsWithValueGreaterThan:(NSNumber *)value
{
    NSPredicate *gtValue =  [NSPredicate predicateWithFormat:@"valueInDollars > %@", value];
    
    return [self possessionsMatchingPredicate:gtValue];
}

- (NSArray *)possessionsWithValueLessThanOrEqualTo:(NSNumber *)value
{
    NSPredicate *lteValue = [NSPredicate predicateWithFormat:@"valueInDollars <= %@", value];
    
    return [self possessionsMatchingPredicate];
}

The ItemViewController uses these methods to determine which items to return for which sections:

#pragma mark TableVIewDataSource protocol methods
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 2;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    NSLog(@"Getting count for section %d", section);
    
    switch (section) {
        case 0:
            return [[[PossessionStore defaultStore] possessionsWithValueLessThanOrEqualTo:[NSNumber numberWithInt:50]] count];
            break;
            
        case 1:
            return [[[PossessionStore defaultStore] possessionsWithValueGreaterThan:[NSNumber numberWithInt:50]] count];
            break;
            
        default:
            return [[[PossessionStore defaultStore] allPossessions] count];
            break;
    }
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    NSLog(@"Getting count for section %d", section);
    
    switch (section) {
        case 0:
            return [[[PossessionStore defaultStore] possessionsWithValueLessThanOrEqualTo:[NSNumber numberWithInt:50]] count];
            break;
            
        case 1:
            return [[[PossessionStore defaultStore] possessionsWithValueGreaterThan:[NSNumber numberWithInt:50]] count];
            break;
            
        default:
            return [[[PossessionStore defaultStore] allPossessions] count];
            break;
    }
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSLog(@"Getting item at index path %@", indexPath);
    
    //check for a reuseable cell
    UITableViewCell *cell =  [tableView dequeueReusableCellWithIdentifier:@"UITableViewCell"];
    
    //create a new cell if we can't find one to resuse
    if (!cell) 
    {
        cell = 
            [[[UITableViewCell alloc] 
              initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"UITableViewCell"] autorelease];
    }
    
    Possession *p;
    
    switch (indexPath.section) 
    {
        case 0:
            p =  [[[PossessionStore defaultStore] 
                   possessionsWithValueLessThanOrEqualTo:[NSNumber numberWithInt:50]] objectAtIndex:indexPath.row];
            break;
            
        case 1:
            p = [[[PossessionStore defaultStore] 
                  possessionsWithValueGreaterThan:[NSNumber numberWithInt:50]] objectAtIndex:indexPath.row];
            break;
            
        default:
            p = [[[PossessionStore defaultStore] allPossessions] objectAtIndex:indexPath.row];
            break;
    }
    
    [[cell textLabel] setText:[p description]];
    
    return cell;
}

Ultimately, I ended up removing the autorelease calls in the above class, since that seemed to cause the application crash. I’m pretty sure that doing this introduced a memory leak. I’m glad to see someone else here try using to use NSPredicate for filtering the items, I think NSPredicate paired with Index Path and Key Value Coding is a really neat technology.


#3

Thanks for the feedback. :smiley: I really like your convenience methods that filter based on the > < values. This could come in handy if you wanted to let the user set the threshold on the fly. I don’t think you created memory leaks by getting rid of your autoreleases. From my understanding filteredArrayUsingPredicate: is a convenience method that returns an autoreleased array already, which is probably why you had the adverse effects when adding your own autorelease, maybe :unamused: . Come to think of it I could probably do without the instance variable “stuff” as well and just return the array from filteredArrayUsingPredicate:. That should be retained by the dictionary in the mySectionsArray anyway. As for filteredArrayUsingPredicate:, I just googled something like “returning subsets of a NSArray” and it was at or close to the top of the list. Seemed like a the best way to go for me. I started out with a system very similar to yours and then someone here, or on Stack Overflow, suggested that if the data set ever got large it would be quite an efficiency hit to rebuild the subarrays with every call to tableview:cellForRowAtIndexPath: so I went with building them on launch and when the data is modified instead. It came at a cost of a few more variables but they are mostly just pointers to pointers to pointers to the possessions anyway. I think the efficiency saved is worth the trade off. Of course I’m still a newb and this could be all wrong :unamused: . Thanks again for your input!!! :wink: