Leak when carrying out operations on annotations in an MKMapView - iphone

I've got a method that takes in the annotations (custom PostLocationAnnotation class) to be displayed on a map view and clusters close ones together, outputting an array of MKAnnotation of PostLocationAnnotations and LocationGroupAnnotations (the clusters, which each contain some PostLocationAnnotations). Here's how I call the function (from within an 'updateAnnotations' method, called when the viewport of the map changes):
[annotationsToAdd addObjectsFromArray:[ffMapView annotations]];
[ffMapView addAnnotations:[self clusterAnnotations:annotationsToAdd WithEpsilon:20.0f andMinPts:4]];
annotationsToAdd is initially populated by the annotations that have been retrieved from the server that have not already been added to the map. Therefore I am passing the full list of the annotations that should be put on the map into the clusterAnnotations method. Here is the body of the method:
- (NSArray *)clusterAnnotations:(NSArray *)annotations WithEpsilon:(float)eps andMinPts:(int)minPts
{
NSMutableSet *D = [[NSMutableSet alloc] initWithCapacity:[annotations count]];
NSMutableArray *C = [[NSMutableArray alloc] init];
for (id <MKAnnotation> annotation in annotations)
{
if ([annotation isKindOfClass:[PostLocationAnnotation class]])
{
NSMutableDictionary *dictEntry = [NSMutableDictionary dictionaryWithObjectsAndKeys:
annotation, #"point",
[NSNumber numberWithBool:NO], #"visited",
[NSNumber numberWithBool:NO], #"noise",
[NSNumber numberWithBool:NO], #"clustered", nil];
[D addObject:dictEntry];
[dictEntry release];
} else if ([annotation isKindOfClass:[LocationGroupAnnotation class]])
{
for (PostLocationAnnotation *location in [(LocationGroupAnnotation *)annotation locations])
{
NSMutableDictionary *dictEntry = [NSMutableDictionary dictionaryWithObjectsAndKeys:
location, #"point",
[NSNumber numberWithBool:NO], #"visited",
[NSNumber numberWithBool:NO], #"noise",
[NSNumber numberWithBool:NO], #"clustered", nil];
[D addObject:dictEntry];
[dictEntry release];
}
}
}
for (NSMutableDictionary *P in D)
{
if ([P objectForKey:#"visited"] == [NSNumber numberWithBool:NO])
{
[P setValue:[NSNumber numberWithBool:YES] forKey:#"visited"];
NSMutableSet *N = [[NSMutableSet alloc] initWithSet:[self regionQueryForPoint:P andEpsilon:eps fromList:D]];
if ([N count] < minPts)
{
[P setValue:[NSNumber numberWithBool:YES] forKey:#"noise"];
} else {
LocationGroupAnnotation *newCluster = [[LocationGroupAnnotation alloc] initWithLocations:nil];
[C addObject:newCluster];
[self expandDbscanClusterWithPoint:P andRegion:N andCluster:newCluster andEpsilon:eps andMinPts:minPts fromList:D];
[newCluster release];
}
[N release];
}
}
NSMutableArray *annotationsToAdd = [[[NSMutableArray alloc] initWithCapacity:[annotations count]] autorelease];
for (NSMutableDictionary *P in D)
{
if ([P objectForKey:#"clustered"] == [NSNumber numberWithBool:NO])
{
[annotationsToAdd addObject:[P objectForKey:#"point"]];
}
}
for (LocationGroupAnnotation *cluster in C)
{
[cluster updateCenterCoordinate];
}
[annotationsToAdd addObjectsFromArray:(NSArray *)C];
[D release];
[C release];
return (NSArray *)annotationsToAdd;
}
When I run this I get a zombie message, and I have found that removing [D release] fixes the zombie but causes a leak. Looking at Instruments I can see that the memory address is first Malloc'd in clusterAnnotations, then retained and released a couple of times, then retained a large number of times by regionQueryForPoint (reaching a peak of 47 references), then released twice by clusterAnnotations, then released by [NSAutoreleasePool drain] until the refcount reaches -1 and I get the zombie message error. Here is the code for regionQueryForPoint:
- (NSSet *)regionQueryForPoint:(NSMutableDictionary *)P andEpsilon:(float)eps fromList:(NSMutableSet *)D
{
NSMutableSet *N = [[[NSMutableSet alloc] init] autorelease];
for (NSMutableDictionary *dictEntry in D)
{
if ((dictEntry != P) &&
([[dictEntry objectForKey:#"point"] isKindOfClass:[PostLocationAnnotation class]]))
{
CGPoint p1 = [ffMapView convertCoordinate:[[P objectForKey:#"point"] coordinate] toPointToView:self.view];
CGPoint p2 = [ffMapView convertCoordinate:[[dictEntry objectForKey:#"point"] coordinate] toPointToView:self.view];
float dX = p1.x - p2.x;
float dY = p1.y - p2.y;
if (sqrt(pow(dX,2)+pow(dY,2)) < eps)
{
[N addObject:dictEntry];
}
}
}
return (NSSet *)N;
}
The large number of retains appear to happen when regionQueryForPoint is called from the expandDbScanClusterWithPoint method, so I've included that here for completeness:
- (void)expandDbscanClusterWithPoint:(NSMutableDictionary *)P andRegion:(NSMutableSet *)N
andCluster:(LocationGroupAnnotation *)cluster
andEpsilon:(float)eps
andMinPts:(int)minPts
fromList:(NSMutableSet *)D
{
[cluster addAnnotation:(PostLocationAnnotation *)[P objectForKey:#"point"]];
[P setValue:[NSNumber numberWithBool:YES] forKey:#"clustered"];
BOOL finished = NO;
while (!finished)
{
finished = YES;
for (NSMutableDictionary *nextP in N)
{
if ([nextP objectForKey:#"visited"] == [NSNumber numberWithBool:NO])
{
[nextP setValue:[NSNumber numberWithBool:YES] forKey:#"visited"];
NSSet *nextN = [self regionQueryForPoint:nextP andEpsilon:eps fromList:D];
if ([nextN count] >= minPts)
{
[N unionSet:nextN];
finished = NO;
break;
}
}
if ([nextP objectForKey:#"clustered"] == [NSNumber numberWithBool:NO])
{
[cluster addAnnotation:[nextP objectForKey:#"point"]];
[nextP setValue:[NSNumber numberWithBool:YES] forKey:#"clustered"];
}
}
}
}
I've been dissecting this for ages, counting references, watching pointers and everything but I just can't work out how to safely release this D set. Can anyone see anything I'm not seeing?

You seem to be over-releasing dictEntry with [dictEntry release];. When using dictionaryWithObjectsAndKeys, you're getting an autoreleased object back. So releasing it again will decrease the retain count.
EDIT: If you're unsure how it works and when you're actually retaining objects, you might want to have a look at the memory management docs:
You create an object using a method whose name begins with “alloc”,
“new”, “copy”, or “mutableCopy” (for example, alloc, newObject, or
mutableCopy).

Related

Populating NSDictionary and NSArrays for Model data

I'm trying to create an NSDictionary full of arrays in the implementation file of my model but my code hasn't worked yet. I want to create arrays that are lists of types of dogs and cats and then add those arrays to a dictionary with keys called DOG and CAT. Here is my code:
#implementation wordDictionary
#synthesize catList = _catList;
#synthesize dogList = _dogList;
#synthesize standardDictionary =_standardDictionary;
- (void)setCatList:(NSMutableArray *)catList
{
self.catList = [NSMutableArray arrayWithObjects:#"lion", #"puma", #"snow leopard", nil];
}
- (void)setDogList:(NSMutableArray *)dogList
{
self.dogList = [NSMutableArray arrayWithObjects:#"pit bull", #"pug", #"chihuahua", nil];
}
-(void)setStandardDictionary:(NSMutableDictionary *)standardDictionary
{
[self.standardDictionary setObject: _catList forKey:#"CAT"];
[self.standardDictionary setObject: _dogList forKey:#"DOG"];
}
- (NSString*)selectKey
{
NSInteger keyCount = [[self.standardDictionary allKeys] count];
NSInteger randomKeyIndex = arc4random() % keyCount;
NSString *randomKey = [[self.standardDictionary allKeys] objectAtIndex:randomKeyIndex];
return randomKey;
}
#end
This code is the model. The model is hooked up to my view controller such that when a user taps a button, the NSString returned from randomKey is displayed in a label on the screen. So the text will read either CAT or DOG. Here's the code for that:
- (IBAction)changeGreeting:(UIButton*)sender {
NSString *chosenKey = [self.dictionary selectKey];
NSString *labelText = [[NSString alloc] initWithFormat:#"%#", chosenKey];
self.label.text = labelText;
}
Unfortunately when I tap the button on the simulator I get an error message saying: Thread 1:EXC_ARITHMETIC (code=EXC_1386_DIV, subcode=0x0) at NSInteger randomKeyIndex = arc4random() % keyCount; and it appears that I'm getting it because neither my NSArray nor my NSDictionary have any objects inside of them.
Does anyone have any idea why my NSArray and NSDictionary haven't been populated?
Thanks very much.
The simple answer is that there isn't any code here that calls the methods to set the arrays or dictionary.
But the real underlying issue is that there are a couple of bad 'patterns' going on here that you should fix:
In your setter methods (setCatList:, setDogList:, setStandardDictionary:) you're not setting the properties in question to the values that are passed in. For example, you should be setting catList to the passed in "catList" variable.
- (void)setCatList:(NSMutableArray *)catList
{
if (_catList != catList) {
[_catList release];
_catList = [catList retain];
}
}
Then you should have some kind of "setup" happening, usually in a method in the view controller like viewDidLoad:
[wordDictionary setCatList:[NSMutableArray arrayWithObjects:#"lion", #"puma", #"snow leopard", nil]];
// and more for the other two setters
Alternately, you can set these default values in the init for the wordDictionary class:
- (id)init {
self = [super init];
if (self) {
[self setCatList:[NSMutableArray arrayWithObjects:#"lion", #"puma", #"snow leopard", nil]];
}
return self;
}
The former is better in most cases, but you may have a good reason to pre-populate your model for all instances of the class.
Assuming you called setCatList:, setDogList: and setStandardDictionary: before. Probably that causing is this :
NSString *chosenKey = [self.dictionary selectKey];
change into this :
NSString *chosenKey = [self selectKey];
UPDATE
I'm trying to make your life easier. no need to create your object if you don't need the most.
- (NSMutableArray*)getCatList
{
return [NSMutableArray arrayWithObjects:#"lion", #"puma", #"snow leopard", nil];
}
- (NSMutableArray*)getDogList
{
return [NSMutableArray arrayWithObjects:#"pit bull", #"pug", #"chihuahua", nil];
}
-(NSMutableDictionary*)getStandardDictionary
{
NSMutableDictionary *standardDictionary = [NSMutableDictionary new];
[standardDictionary setObject:[self getCatList] forKey:#"CAT"];
[standardDictionary setObject:[self getDogList] forKey:#"DOG"];
return [standardDictionary autorelease];
}
- (NSString*)selectKey
{
NSMutableDictionary *standardDictionary = [self getStandardDictionary];
NSInteger keyCount = [[standardDictionary allKeys] count];
NSInteger randomKeyIndex = arc4random() % keyCount;
NSString *randomKey = [[standardDictionary allKeys] objectAtIndex:randomKeyIndex];
return randomKey;
}
- (IBAction)changeGreeting:(UIButton*)sender {
// NSString *chosenKey = [self selectKey];
//NSString *labelText = [[NSString alloc] initWithFormat:#"%#", chosenKey];
self.label.text = [self selectKey]; //no need to convert it to NSString again
}
Two things to consider:
I don't see you calling these:
setCatList:(NSMutableArray*)catList;
setDogList:(NSMutableArray*)dogList;
You use self.catList and self.dogList, but neither of those are synthesized, instead you have beatList and meList synthesized
Change the synthesizes to the catList and dogList, and make sure you call the set list methods, and then you should make some progress.

Local high scores reporting/Display

Please can anyone refer me to a tutorial or help me out with this problem. I wish to display high scores and other games statistics at game Over but I don't seem to get a headway despite looking at tutorials and trying stuff out. I don't seem to understand how to use NSUserdefaults to achieve this.
My codes are as follows:
Gameplaylayer.mm
- (id)initWithHUDLayer:(HUDLayer *)hudLayer {
if ((self = [super init]))
{
score = 0;
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSDictionary *defaultArray = [NSDictionary dictionaryWithObject:[NSArray arrayWithObjects:[NSNumber numberWithInt:0],
[NSNumber numberWithInt:0],
[NSNumber numberWithInt:0],
[NSNumber numberWithInt:0],
[NSNumber numberWithInt:0],
nil]
forKey:#"Scores"];
[defaults registerDefaults:defaultArray];
[defaults synchronize];
}
return self;
}
-(void)gameOver:(id)sender{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSMutableArray *HighScores = [NSMutableArray arrayWithArray:[defaults arrayForKey:#"Scores"]];
for (unsigned int i = 0; i < [HighScores count]; i++)
{
if (dist >= [[HighScores objectAtIndex:i] intValue])
{
[HighScores insertObject:[NSNumber numberWithInt:dist] atIndex:i];
[HighScores removeLastObject];
[defaults setObject:HighScores forKey:#"Scores"];
[defaults synchronize];
NSLog(#"Saved new high score of %i", dist);
break;
}
}
}
-(void)update:(ccTime)dt {
CGSize winSize = [CCDirector sharedDirector].winSize;
score += (int)delta;
dist = score - 18;
if (!gameOver )
{
if (!lives)
{
gameOver = true;
[self gameOver];
}
}
}
my game over layer is as follows
-(id)init {
self = [super init];
if (self != nil)
{
self.isTouchEnabled = YES;
CGSize windowSize = [CCDirector sharedDirector].winSize;
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSArray *HighScores = [defaults arrayForKey:#"Scores"];
CLabelTTF *title = [CCLabelTTF labelWithString:#"high scores" fontName:#"Courier" fontSize:32.0];
[title setPosition:ccp(screenSize.width / 2, screenSize.height - title.contentSize.height)];
[self addChild:title];
NSMutableString *scoresString = [NSMutableString stringWithString:#""];
for (unsigned int i = 0; i < [HighScores count]; i++)
{
[scoresString appendFormat:#"%i. %i\n", i + 1, [[HighScores objectAtIndex:i] intValue]];
}
CCLOG(#"scores saved %i", dist);
CCLabelTTF *scoresLabel = [CCLabelTTF labelWithString:scoresString dimensions:CGSizeMake(screenSize.width, screenSize.height / 3) alignment:CCTextAlignmentCenter fontName:#"Courier" fontSize:40.0];
[scoresLabel setPosition:ccp(screenSize.width / 2, screenSize.height / 2)];
scoresLabel.color = ccc3(0, 0, 0);
[self addChild:scoresLabel z:9];
}
return self;
}
It's hard to understand your code, please look at this small example below.
Ok, that's saving new score, assuming you have GameController singleton, where scores mutable array is declared and currentScore in that singleton also displays current player's score.
-(void) saveScores {
GameController * controller = [GameController sharedController];
for(int i=0; i<5;i++)
{
if([[[controller.scores objectAtIndex:i] objectForKey:#"score"] intValue] == controller.finalscore)
{
break;
}
if([[[controller.scores objectAtIndex:i] objectForKey:#"score"] intValue] < controller.finalscore) {
for(int j=5;j>i+1;j--)
{
[controller.scores replaceObjectAtIndex:j-1 withObject:[controller.scores objectAtIndex:j-2]];
}
NSMutableDictionary *newEntry = [NSMutableDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:controller.finalscore],#"score",#"Local player",#"name", nil];
[controller.scores replaceObjectAtIndex:i withObject:newEntry];
break;
}
}
controller.currentScore=0;
[[NSUserDefaults standardUserDefaults] setObject:controller.scores forKey:#"OurScores"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
You can see, that we are saving updated scores in NSUserDefaults. So, at our game start we shoul load them. I added this code to GameController init method:
- (void) loadScores {
if([[NSUserDefaults standardUserDefaults] objectForKey:#"OurScores"] == nil)
{
NSMutableDictionary *empty = [NSMutableDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:0],#"score",#"empty",#"name", nil];
for(int i=0;i<5;i++)
[scores addObject:empty];
}
else
{
NSLog(#"Loading scores");
scores = [[NSUserDefaults standardUserDefaults] objectForKey:#"OurScores"];
}
}
And that's how we can show this scores using cocos2d:
-(void) drawScores {
for(int i=0; i<5; i++)
{
CCLabelTTF* score = [CCLabelTTF labelWithString:
[NSString stringWithFormat:#"%#",[[controller.scores objectAtIndex:i] objectForKey:#"name"]] fontName:#"Marker Felt" fontSize:30];
score.anchorPoint=ccp(0,0);
score.position = ccp(115, 180-i*35);
CCLabelTTF* score2 = [CCLabelTTF labelWithString:
[NSString stringWithFormat:#"%d",[[[controller.scores objectAtIndex:i] objectForKey:#"score"] intValue] ] fontName:#"Marker Felt" fontSize:35];
score2.anchorPoint=ccp(0,0);
score2.position = ccp(280, 180-i*35);
score.color=currentClr;
score2.color=currentClr;
[self addChild: score z:3 tag:i];
[self addChild: score2 z:3 tag:i+15];
}
}
Your code has a number of problems, some of which just break with convention but all together they make your program quite unreadable. I have not been able to make sense of it. You are not using comments, nor are you telling us, what exactly is not working.
For instance, you might consider following the convention of having instance variables start with small letters, reserving capitalized names for classes. It is generally a bad idea to give misleading names, such as calling an NSDictinoary "defaultArray". Also, try to use empty lines more sparingly to structure your code for better readability.
That being said, this is how you set a default value:
[[NSUserDefaults standardUserDefaults] setValue:scoreArray
forKey:#"Score"];
I see you know how to retrieve the score.
As for your updating routine, consider that it is normally not a good idea to modify the array your iterating through (in your particular case it could still work, but it is not guaranteed). Also, there is no need to store the whole array in the loop. Just save it once you have contructed the complete new high score array.
Otherwise your code is working, right?

How to define a static table of strings in iPhone / XCode?

I want a table of state-names for debug/trace messages as I develop my custom gesture. What is wrong with this declaration and its usage?
static NSDictionary *dictStateNames = nil;
#implementation MyCustomGesture
#synthesize state;
+(void)initStateNames {
if (dictStateNames == nil) {
dictStateNames = [NSDictionary dictionaryWithObjectsAndKeys:
#"StateBegan", [NSNumber numberWithInt:UIGestureRecognizerStateBegan],
#"StateCancelled", [NSNumber numberWithInt:UIGestureRecognizerStateCancelled],
#"StateChanged", [NSNumber numberWithInt:UIGestureRecognizerStateChanged],
#"StateEnded", [NSNumber numberWithInt:UIGestureRecognizerStateEnded],
#"StateFailed", [NSNumber numberWithInt:UIGestureRecognizerStateFailed],
#"StatePossible", [NSNumber numberWithInt:UIGestureRecognizerStatePossible],
#"StateRecognized", [NSNumber numberWithInt:UIGestureRecognizerStateRecognized],
nil];
}
}
-(id) init {
self = [super init];
if (self) {
[MyCustomGesture initStateNames];
state = UIGestureRecognizerStatePossible;
}
return self;
}
-(id) initWithTarget:(id)target action:(SEL)action {
self = [super initWithTarget:target action:action];
if (self) {
[MyCustomGesture initStateNames];
state = UIGestureRecognizerStatePossible;
}
return self;
}
+(NSString*) stateName: (UIGestureRecognizerState) state {
NSString *retName = [dictStateNames objectForKey:[NSNumber numberWithInt:state]];
if (retName == nil) {
return [NSString stringWithFormat:#"Unknown state (%#)", state];
} else {
return retName;
}
}
-(NSString*) currentStateName {
return [MyCustomGesture stateName:state];
}
-(void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#"%s (%#): %#", __FUNCTION__, [self currentStateName], event);
}
When you store a reference to an object in a static variable, you need to ensure that it doesn't get deallocated. So you can either send it a retain message, or create with alloc instead of a convenience creation method. For example:
dictStateNames = [[NSDictionary dictionaryWithObjectsAndKeys:
// List of objects and keys...
nil] retain];
or this...
dictStateNames = [NSDictionary alloc] initWithObjectsAndKeys:
// List of objects and keys...
nil];
Also, you can coalesce your stateNames getter and the initialization code into a single method, so you'll typically see Objective-C developers write a method like this:
+ (NSDictionary *)stateNames
{
static NSDictionary *stateNames;
if (stateNames == nil) {
stateNames = [NSDictionary alloc] initWithObjectsAndKeys:
// List of objects and keys...
nil];
}
return stateNames;
}
That way, there's no need to call it in an instance method (which would be wrong anyway, since a new dictionary would be created each time an instance is initialized, and unless you handled it differently, the previous one would be leaked).
On another (unrelated) note, consider rewriting your init method as follows:
- (id)init
{
return [self initWithTarget:nil action:NULL];
}
[NSDictionary dictionaryWithObjectsAndKeys] takes an object first followed by its key (as the method name suggests), which seems a bit backward but there it is.

NSArray sort and isolate

I have an NSArray of names, I want to sort them alphabetically into a UITableView and separate them into sections.
I have a tagged section at the top, being section 0. I want the names sorted aplhabetically to come after that. So all names beginning with A get put into section 1, B into section 2, and so on.
I need to be able to somehow get the number of rows for each section, and then put the objects in each section.
How do I do this?
Here is a method for a category on NSArray to do grouping:
#interface NSArray (Grouping)
- (NSArray*) groupUsingFunction: (id (*)(id, void*)) function context: (void*) context;
#end
#implementation NSArray (Grouping)
- (NSArray*) groupUsingFunction: (id (*)(id, void*)) function context: (void*) context
{
NSArray* groupedArray = nil;
NSMutableDictionary* dictionary = [NSMutableDictionary new];
if (dictionary != nil)
{
for (id item in self)
{
id key = function(item, context);
if (key != nil)
{
NSMutableArray* array = [dictionary objectForKey: key];
if (array == nil) {
array = [NSMutableArray arrayWithObject: item];
if (array != nil) {
[dictionary setObject: array forKey: key];
}
} else {
[array addObject: item];
}
}
}
groupedArray = [NSMutableArray arrayWithArray: [dictionary allValues]];
[dictionary release];
}
return groupedArray;
}
#end
You can use it like this:
id GroupNameByFirstLetter(NSString* object, void* context)
{
return [object substringToIndex: 1];
}
NSInteger SortGroupedNamesByFirstLetter(id left, id right, void* context)
{
return [[left objectAtIndex: 0] characterAtIndex: 0] - [[right objectAtIndex: 0] characterAtIndex: 0];
}
NSMutableArray* names = [NSArray arrayWithObjects: #"Stefan", #"John", #"Alex",
#"Sue", #"Aura", #"Mikki", #"Michael", #"Joe", #"Steve", #"Mac", #"Fred",
#"Faye", #"Paul", nil];
// Group the names and then sort the groups and the contents of the groups.
groupedNames_ = [[names groupUsingFunction: GroupNameByFirstLetter context: nil] retain];
[groupedNames_ sortUsingFunction: SortGroupedNamesByFirstLetter context: nil];
for (NSUInteger i = 0; i < [groupedNames_ count]; i++) {
[[groupedNames_ objectAtIndex: i] sortUsingSelector: #selector(compare:)];
}
I modified St3fans answer to be a bit more modern and work with Blocks instead:
#interface NSArray (Grouping)
- (NSArray*) groupUsingBlock:(NSString* (^)(id object)) block;
#end
- (NSArray*) groupUsingBlock:(NSString* (^)(id object)) block
{
NSArray* groupedArray = nil;
NSMutableDictionary* dictionary = [NSMutableDictionary new];
if (dictionary != nil)
{
for (id item in self)
{
id key = block(item);
if (key != nil)
{
NSMutableArray* array = [dictionary objectForKey: key];
if (array == nil) {
array = [NSMutableArray arrayWithObject: item];
if (array != nil) {
[dictionary setObject: array forKey: key];
}
} else {
[array addObject: item];
}
}
}
groupedArray = [NSMutableArray arrayWithArray: [dictionary allValues]];
[dictionary release];
}
return groupedArray;
}
You can use it like this:
NSArray *grouped = [arrayToGroup groupUsingBlock:^NSString *(id object) {
return [object valueForKey:#"name"];
}];
You should probably create an array of arrays, one for each letter, and store your names that way. While you can use a single array for storage, there's no quick way to do the segmentation you're looking for. Sorting, sure, but not section-ization.

Advice on High Score persistence (iPhone, Cocoa Touch)

I was curious what is considered the better way to manage the reading and writing of a High Score plist file. My High Score class is:
#interface HighScore : NSObject <NSCoding> {
NSString *name;
int score;
int level;
int round;
NSDate *date;
}
Now, I could do method A, add NSCoding methods:
- (void) encodeWithCoder: (NSCoder *) coder {
[coder encodeObject: name
forKey: kHighScoreNameKey];
[coder encodeInt: score
forKey: kHighScoreScoreKey];
[coder encodeInt: level
forKey: kHighScoreLevelKey];
[coder encodeInt: round
forKey: kHighScoreRoundKey];
[coder encodeObject: date
forKey: kHighScoreDateKey];
} // encodeWithCoder
- (id) initWithCoder: (NSCoder *) decoder {
if (self = [super init]) {
self.name = [decoder decodeObjectForKey: kHighScoreNameKey];
self.score = [decoder decodeIntForKey: kHighScoreScoreKey];
self.level = [decoder decodeIntForKey: kHighScoreLevelKey];
self.round = [decoder decodeIntForKey: kHighScoreRoundKey];
self.date = [decoder decodeObjectForKey: kHighScoreDateKey];
}
return (self);
} // initWithCoder
And write it all out with:
if (![NSKeyedArchiver archiveRootObject:highScoresList toFile:path]) ...
Reading it back in would be pretty straight forward. However the plist file, IMHO, looks like crap.
Or I could employ method B:
NSMutableArray *array = [NSMutableArray arrayWithCapacity:20];;
for (HighScore *hs in highScoresList) {
NSDictionary *dict = [[NSDictionary alloc] initWithObjectsAndKeys:
hs.name, kHighScoreNameKey,
[NSNumber numberWithInteger:hs.score], kHighScoreScoreKey,
[NSNumber numberWithInteger:hs.level], kHighScoreLevelKey,
[NSNumber numberWithInteger:hs.round], kHighScoreRoundKey,
hs.date, kHighScoreDateKey,
nil];
[array addObject:dict];
[dict release];
}
and write it all out with:
if (![array writeToFile:path atomically:YES]) ...
Reading it back in is a tiny bit harder. But the plist file looks much cleaner (smaller and compact).
Any thoughts? Am I missing something that is much simpler? (I want to keep the High Scores separate from NSUserDefaults so I am not using that).
Both your ways look fine to me. There is also Core Data in the 3.0 OS, although it might be overkill if all you want to save is a single high score value.
I'm not sure that I understand your objections! Both should work just fine.
Personally I prefer method A. I think it makes sense that an object knows how to encode itself. It makes maintenance easier and any changes more localised. Plus it probably uses less memory.
check out this question, maybe it's useful for your app
I went with method B because: 1. The plist file is more readable and 2. I can save off some file and class version numbering into this method easily:
In my HighScore class:
- (id)initFromDictionary: (NSDictionary *)dict;
{
if (self = [super init]) {
self.name = [dict objectForKey:kHighScoreNameKey];
self.score = [[dict objectForKey:kHighScoreScoreKey] integerValue];
self.game = [[dict objectForKey:kHighScoreGameKey] integerValue];
self.level = [[dict objectForKey:kHighScoreLevelKey] integerValue];
self.date = [dict objectForKey:kHighScoreDateKey];
}
return (self);
}
- (NSDictionary *)putIntoDictionary;
{
NSDictionary *dict = [[NSDictionary alloc] initWithObjectsAndKeys:
name, kHighScoreNameKey,
[NSNumber numberWithInt:score], kHighScoreScoreKey,
[NSNumber numberWithInt:game], kHighScoreGameKey,
[NSNumber numberWithInt:level], kHighScoreLevelKey,
date, kHighScoreDateKey,
nil];
return dict;
}
And in my HighScoreTable class:
- (id) load
{
NSString *path = [self getFilePath];
// [self clear];
NSDictionary *rootLevelPlistDict = [NSDictionary dictionaryWithContentsOfFile:path];
int versNum = [[rootLevelPlistDict objectForKey:kHighScoreVersKey] integerValue];
if (versNum == kHighScoreVersNum) {
NSArray *insideArray = [rootLevelPlistDict objectForKey:kHighScoresKey];
NSDictionary *dict;
for (dict in insideArray) {
HighScore *hs = [[HighScore alloc] initFromDictionary:dict];
[highScoresList addObject:hs];
[hs release];
}
}
return sharedHighScoresSingleton;
}
- (void) save
{
if (!changed)
return;
NSString *path = [self getFilePath];
NSMutableArray *array = [NSMutableArray arrayWithCapacity:kNumberOfHighScores];
for (HighScore *hs in highScoresList) {
NSDictionary *dict = [hs putIntoDictionary];
[array addObject:dict];
[dict release];
}
NSDictionary *rootLevelPlistDict = [[NSDictionary alloc] initWithObjectsAndKeys:
[[[NSBundle mainBundle] infoDictionary]
objectForKey:(NSString*)kCFBundleNameKey], kHighScoreAppNameKey,
[NSNumber numberWithInt:kHighScoreHeaderVersNum], kHighScoreHeaderVersKey,
[NSNumber numberWithInt:kHighScoreVersNum], kHighScoreVersKey,
[NSDate date], kHighScoreCreationKey,
array, kHighScoresKey,
nil];
if (![rootLevelPlistDict writeToFile:path atomically:YES])
NSLog(#"not successful in writing the high scores");
[rootLevelPlistDict release];
}