Getting array elements with valueForKeyPath - objective-c

Is there any way to access an NSArray element with valueForKeyPath? Google's reverse geocoder service, for example, returns a very complex data structure. If I want to get the city, right now I have to break it into two calls, like this:
NSDictionary *address = [NSString stringWithString:[[[dictionary objectForKey:#"Placemark"] objectAtIndex:0] objectForKey:#"address"]];
NSLog(#"%#", [address valueForKeyPath:#"AddressDetails.Country.AdministrativeArea.SubAdministrativeArea.Locality.LocalityName"]);
Just wondering if there's a way to shoehorn the objectAtIndex: call into the valueForKeyPath string. I tried a javascript-esque formulation like #"Placemark[0].address" but no dice.

Unfortunately, no. The full documentation for what's allowed using Key-Value Coding is here. There are not, to my knowledge, any operators that allow you to grab a particular array or set object.

Here's a category I just wrote for NSObject that can handle array indexes so you can access a nested object like this: "person.friends[0].name"
#interface NSObject (ValueForKeyPathWithIndexes)
-(id)valueForKeyPathWithIndexes:(NSString*)fullPath;
#end
#import "NSObject+ValueForKeyPathWithIndexes.h"
#implementation NSObject (ValueForKeyPathWithIndexes)
-(id)valueForKeyPathWithIndexes:(NSString*)fullPath
{
NSRange testrange = [fullPath rangeOfString:#"["];
if (testrange.location == NSNotFound)
return [self valueForKeyPath:fullPath];
NSArray* parts = [fullPath componentsSeparatedByString:#"."];
id currentObj = self;
for (NSString* part in parts)
{
NSRange range1 = [part rangeOfString:#"["];
if (range1.location == NSNotFound)
{
currentObj = [currentObj valueForKey:part];
}
else
{
NSString* arrayKey = [part substringToIndex:range1.location];
int index = [[[part substringToIndex:part.length-1] substringFromIndex:range1.location+1] intValue];
currentObj = [[currentObj valueForKey:arrayKey] objectAtIndex:index];
}
}
return currentObj;
}
#end
Use it like so
NSString* personsFriendsName = [obj valueForKeyPathsWithIndexes:#"me.friends[0].name"];
There's no error checking, so it's prone to breaking but you get the idea.

You can intercept the keypath in the object holding the NSArray.
In your case the keypath would become Placemark0.address... Override valueForUndefinedKey; look for the index in the keypath; something like this:
-(id)valueForUndefinedKey:(NSString *)key
{
// Handle paths like Placemark0, Placemark1, ...
if ([key hasPrefix:#"Placemark"])
{
// Caller wants to access the Placemark array.
// Find the array index they're after.
NSString *indexString = [key stringByReplacingOccurrencesOfString:#"Placemark" withString:#""];
NSInteger index = [indexString integerValue];
// Return array element.
if (index < self.placemarks.count)
return self.placemarks[index];
}
return [super valueForUndefinedKey:key];
}
This works really well for model frameworks e.g. Mantle.

Subclass NSArrayController or NSDictionaryController
Use NSArrayController for this purpose, because NSObjectController does not include NSArrayController's provided handling of changes to bound array elements. If you use this same code with NSObjectController instead, then using Cocoa Bindings with your NSObjectController instance will only set the (bound interface element's) value at the time of binding but will not receive the messages from array elements in return. By using NSObjectController for this purpose, the user interface will not continue to update even though the contentObject is updated. Simply use the same code with NSArrayController to also include proper support for arrays -- which is the matter at hand.
#import <Cocoa/Cocoa.h>
#interface DelvingArrayController : NSArrayController
#end
#import "DelvingArrayController.h"
#implementation DelvingArrayController
-(id)valueForKeyPath:(NSString *)keyPath
{
NSError *error = nil;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:#"^(.+?)\\[(\\d+?)\\]$" options:NSRegularExpressionCaseInsensitive error:&error];
NSArray<NSString*> *components = [keyPath componentsSeparatedByString:#"."];
id currentObject = self;
for (NSUInteger i = 0; i < components.count; i++)
{
if (![components[i] isEqualToString:#""])
{
NSTextCheckingResult *check_result = [regex firstMatchInString:components[i] options:0 range:NSMakeRange(0, components[i].length)];
if (!check_result)
currentObject = [currentObject valueForKey:components[i]];
else
{
NSRange array_name_capture_range = [check_result rangeAtIndex:1];
NSRange number_capture_range = [check_result rangeAtIndex:2];
if (number_capture_range.location == NSNotFound)
currentObject = [currentObject valueForKey:components[i]];
else if (array_name_capture_range.location != NSNotFound)
{
NSString *array_name = [components[i] substringWithRange:array_name_capture_range];
NSUInteger array_index = [[components[i] substringWithRange:number_capture_range] integerValue];
currentObject = [currentObject valueForKey:array_name];
if ([currentObject count] > array_index)
currentObject = [currentObject objectAtIndex:array_index];
}
}
}
}
return currentObject;
}
//at some point... also override setValueForKeyPath :-)
#end
This code uses NSRegularExpression, which is for macOS 10.7+.
I leave it as an exercise for you to use the same approach to also override setValueForKeyPath, if you want write functionality.
Cocoa Bindings Example Usage
Say we want a little trivia game, with a window that shows a question and uses four buttons to display multiple-choice options. We have the questions and multiple-choice options as NSStrings in a plist, and also an NSNumber or optionally BOOL entries to indicate the correct answers. We want to bind the option buttons to options in the array, for each question also stored in an array.
Here is the example plist containing some trivia questions related to the game Halo. Notice that the options are located within nested arrays.
In this example, I use NSObjectController *stringsController as the controller for the entire plist file, and DelvingArrayController *triviaController as the controller for the trivia-related plist entries. You might simply use one DelvingArrayController instead, but I provide this for your understanding.
The trivia window is really simple, so I merely design it using Interface Builder in MainMenu.xib:
A subclass of NSDocumentController is used for showing the trivia window via an NSMenuItem added in Interface Builder. The instance of this subclass is also in the .xib, so if we want to use the interface elements in the .xib, we have to wait for the Application Delegate instance's - (void)applicationDidFinishLaunching:(NSNotification *)aNotification method or otherwise wait until the .xib has finished loading...
#import <Cocoa/Cocoa.h>
#import "MenuInterfaceDocumentController.h"
#interface AppDelegate : NSObject <NSApplicationDelegate>
#property IBOutlet MenuInterfaceDocumentController *PrimaryInterfaceController;
#end
#import "AppDelegate.h"
#interface AppDelegate ()
#end
#implementation AppDelegate
#synthesize PrimaryInterfaceController;
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
if ([NSApp mainMenu])
{
[PrimaryInterfaceController configureTriviaWindow];
}
}
#import <Cocoa/Cocoa.h>
#interface MenuInterfaceDocumentController : NSDocumentController
{
IBOutlet NSMenuItem *MenuItemTrivia; // shows the Trivia window
IBOutlet NSWindow *TriviaWindow;
IBOutlet NSTextView *TriviaQuestionField;
IBOutlet NSButton *TriviaOption1, *TriviaOption2, *TriviaOption3, *TriviaOption4;
}
#property NSObjectController *stringsController;
-(void)configureTriviaWindow;
#end
#import "MenuInterfaceDocumentController.h"
#interface MenuInterfaceDocumentController ()
#property NSDictionary *languageDictionary;
#property DelvingArrayController *triviaController;
#property NSNumber *triviaAnswer;
#end
#implementation MenuInterfaceDocumentController
#synthesize stringsController, languageDictionary, triviaController, triviaAnswer;
// all this happens before the MainMenu is available, and before the AppDelegate is sent applicationDidFinishLaunching
-(instancetype)init
{
self = [super init];
if (self)
{
if (!stringsController)
stringsController = [NSObjectController new];
stringsController.editable = NO;
// check for the plist file, eventually applying the following
languageDictionary = [NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"en" ofType:#"plist"]];
if (languageDictionary)
[stringsController setContent:languageDictionary];
if (!triviaController)
{
triviaController = [DelvingArrayController new];
[triviaController bind:#"contentArray" toObject:stringsController withKeyPath:#"selection.trivia" options:nil];
}
triviaController.editable = NO;
if (!triviaAnswer)
{
triviaAnswer = #0;
[self bind:#"triviaAnswer" toObject:triviaController withKeyPath:#"selection.answer" options:nil];
}
}
return self;
}
// if we ever do something like change the plist file to a duplicate plist file that is in a different language, use this kind of approach to keep the same trivia entry active
-(IBAction)changeLanguage:(id)sender
{
NSUInteger triviaQIndex = triviaController.selectionIndex;
if (sender == MenuItemEnglishLanguage)
{
if ([self changeLanguageTo:#"en" Notify:YES])
{
[self updateSelectedLanguageMenuItemWithLanguageString:#"en"];
if ([triviaController.content count] > triviaQIndex) // in case the plist files don't match
[triviaController setSelectionIndex:triviaQIndex];
}
else
[self displayAlertFor:CUSTOM_ALERT_TYPE_LANGUAGE_CHANGE_FAILED];
}
else if (sender == MenuItemGermanLanguage)
{
if ([self changeLanguageTo:#"de" Notify:YES])
{
[self updateSelectedLanguageMenuItemWithLanguageString:#"de"];
if ([triviaController.content count] > triviaQIndex)
[triviaController setSelectionIndex:triviaQIndex];
}
else
[self displayAlertFor:CUSTOM_ALERT_TYPE_LANGUAGE_CHANGE_FAILED];
}
}
-(void)configureTriviaWindow
{
[TriviaQuestionField bind:#"string" toObject:triviaController withKeyPath:#"selection.question" options:nil];
[TriviaOption1 bind:#"title" toObject:triviaController withKeyPath:#"selection.options[0]" options:nil];
[TriviaOption2 bind:#"title" toObject:triviaController withKeyPath:#"selection.options[1]" options:nil];
[TriviaOption3 bind:#"title" toObject:triviaController withKeyPath:#"selection.options[2]" options:nil];
[TriviaOption4 bind:#"title" toObject:triviaController withKeyPath:#"selection.options[3]" options:nil];
}
// this method is how you would manually set the value if you did not use binding:
-(void)updateTriviaAnswer
{
triviaAnswer = [triviaController valueForKeyPath:#"selection.answer"];
}
-(IBAction)changeTriviaQuestion:(id)sender
{
if (triviaController.selectionIndex >= [(NSArray*)triviaController.content count] - 1)
[triviaController setSelectionIndex:0];
else
[triviaController setSelectionIndex:(triviaController.selectionIndex + 1)];
}
-(IBAction)showTriviaWindow:(id)sender
{
[TriviaWindow makeKeyAndOrderFront:sender];
}
- (IBAction)TriviaOptionChosen:(id)sender
{
// tag integers 0 through 3 are assigned to the option buttons in Interface Builder
if ([sender tag] == triviaAnswer.integerValue)
[self changeTriviaQuestion:sender];
else
NSBeep();
}
#end
Summary of Sequence
NSObjectController *stringsController = [[NSObjectController alloc] initWithContent:[NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"en" ofType:#"plist"]]];
DelvingArrayController *triviaController = [DelvingArrayController new];
[triviaController bind:#"contentArray" toObject:stringsController withKeyPath:#"selection.trivia" options:nil];
NSNumber *triviaAnswer = #0;
[self bind:#"triviaAnswer" toObject:triviaController withKeyPath:#"selection.answer" options:nil];
// bind to .xib's interface elements after the nib has finished loading, else the IBOutlets are null
[TriviaQuestionField bind:#"string" toObject:triviaController withKeyPath:#"selection.question" options:nil];
[TriviaOption1 bind:#"title" toObject:triviaController withKeyPath:#"selection.options[0]" options:nil];
[TriviaOption2 bind:#"title" toObject:triviaController withKeyPath:#"selection.options[1]" options:nil];
[TriviaOption3 bind:#"title" toObject:triviaController withKeyPath:#"selection.options[2]" options:nil];
[TriviaOption4 bind:#"title" toObject:triviaController withKeyPath:#"selection.options[3]" options:nil];
// when the user chooses the correct option, go to the next question
if ([sender tag] == triviaAnswer.integerValue)
{
if (triviaController.selectionIndex >= [(NSArray*)triviaController.content count] - 1)
[triviaController setSelectionIndex:0];
else
[triviaController setSelectionIndex:(triviaController.selectionIndex + 1)];
}

Create methods that supports array for NSObject:
#interface NSObject(ArraySupported)
-(id)valueForKeySupportedArray:(NSString*)path;
-(id)valueForKeyPathSupportedArray:(NSString*)fullPath;
#end
#implementation NSObject(ArraySupported)
-(id)valueForKeySupportedArray:(NSString*)path {
id value = nil;
if ([self isKindOfClass:[NSArray class]]) {
NSArray *array = (NSArray *)self;
NSUInteger index = path.integerValue;
if (index >= 0 && index < array.count) {
value = array[index];
}
} else {
value = [self valueForKey:path];
}
return value;
}
-(id)valueForKeyPathSupportedArray:(NSString*)fullPath {
NSArray* parts = [fullPath componentsSeparatedByString:#"."];
id value = self;
for (NSString* part in parts) {
value = [value valueForKeySupportedArray:part];
if (value == nil) {
break;
}
}
return value;
}
#end
How to use:
NSObject *object = #{#"Placemark":#[#{#"address":#"..."}]};
NSString *address = [object valueForKeyPathSupportedArray:#"Placemark.0.address"];
// address = "..."

Related

Duplicated custom object in NSSet

I have some problems about the NSMutableSet in Objective-C.
I learnt that the NSSet will compare the two objects' hash code to decide whether they are identical or not.
The problems is, I implemented a class that is subclass of NSObject myself. There is a property NSString *name in that class. What I want to do is when instances of this custom class has the same variable value of "name" , they should be identical, and such identical class should not be duplicated when adding to an NSMutableSet.
So I override the - (NSUInteger)hash function, and the debug shows it returns the same hash for my two instances obj1, obj2 (obj1.name == obj2.name). But when I added obj1, obj2 to an NSMutableSet, the NSMutableSet still contained both obj1, obj2 in it.
I tried two NSString which has the same value, then added them to NSMutableSet, the set will only be one NSString there.
What could be the solution? Thank you for any help!
The custom Class:
Object.h:
#import <Foundation/Foundation.h>
#interface Object : NSObject
#property (retain) NSString *name;
#end
Object.m
#implementation Object
#synthesize name;
-(BOOL)isEqualTo:(id)obj {
return [self.name isEqualToString:[(Object *)obj name]] ? true : false;
}
- (NSUInteger)hash {
return [[self name] hash];
}
#end
and main:
#import <Foundation/Foundation.h>
#import "Object.h"
int main(int argc, const char * argv[])
{
#autoreleasepool {
Object *obj1 = [[Object alloc]init];
Object *obj2 = [[Object alloc]init];
obj1.name = #"test";
obj2.name = #"test";
NSMutableSet *set = [[NSMutableSet alloc] initWithObjects:obj1, obj2, nil];
NSLog(#"%d", [obj1 isEqualTo:obj2]);
NSLog(#"%ld", [set count]);
}
return 0;
}
Instead of implementing isEqualTo: you have to implement isEqual:
- (BOOL)isEqual:(id)object {
return [object isKindOfClass:[MyObject class]] &&
[self.name isEqual:[(MyObject *)object name]];
}
This will (probably falsely) return NO if both self.name and object.name are nil. If you want to return YES if both properties are nil you should use
- (BOOL)isEqual:(id)object {
if ([object isKindOfClass:[MyObject class]]) {
return (!self.name && ![(MyObject *)object name]) ||
[self.name isEqual:[(MyObject *)object name]];
}
return NO;
}

While (not) loop freezes app

My while loop doesn't seem to work. When loading this view, the app freezes.
When I delete the part of code, containing the while loop, the app won't freeze.
What I'm searching for is a piece of code that will cause that the same array is not chosen twice.
#interface ThirdViewController ()
#end
#implementation ThirdViewController
...
NSString * Answer = #"";
NSArray * RAMArray;
...
- (void)NewQuestion
{
NSString * PlistString = [[NSBundle mainBundle] pathForResource:#"Questions" ofType:#"plist"];
NSMutableArray * PlistArray = [[NSMutableArray alloc]initWithContentsOfFile:PlistString];
NSArray *PlistRandom = [PlistArray objectAtIndex: random()%[PlistArray count]];
while (![PlistRandom isEqual: RAMArray])
{
NSArray *PlistRandom = [PlistArray objectAtIndex: random()%[PlistArray count]];
}
RAMArray = PlistRandom;
...
}
- (void)Check:(NSString*)Choise
{
...
if ([Choise isEqualToString: Answer])
{
...
[self NewQuestion];
}
}
- (IBAction)AnsButA:(id)sender
{
UIButton *ResultButton = (UIButton *)sender;
NSString *Click = ResultButton.currentTitle;
[self Check:Click];
}
My guess is that because you re-declare PlistRandom within the while loop, the inner-declared variable may be out of scope at the point the while conditionis evaluated. Your problem I think is a scoping issue, just change the loop to this and see if that works:
while (![PlistRandom isEqual: RAMArray])
{
PlistRandom = [PlistArray objectAtIndex: random()%[PlistArray count]];
}

EXC_BAD_ACCESS error when using Core Data when changing attribute value

Im pretty new to Core Data programming and Cocoa in general, so no wonder I'm having troubles :)
So here is my managedObjectModel method:
- (NSManagedObjectModel *)managedObjectModel
{
if (managedObjectModel != nil)
{
return managedObjectModel;
}
NSString *modelPath = [[NSBundle mainBundle] pathForResource:#"Model" ofType:#"momd"];
NSURL *modelURL = [NSURL fileURLWithPath:modelPath];
NSAssert(modelURL != nil,#"modelURL == nil");
managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
return managedObjectModel;
}
Here is the part of the code that crashes:
NSManagedObjectModel *mom = [self managedObjectModel];
managedObjectModel = mom;
if (applicationLogDirectory() == nil)
{
NSLog(#"Could not find application logs directory\nExiting...");
exit(1);
}
NSManagedObjectContext *moc = [self managedObjectContext];
NSProcessInfo *processInfo = [NSProcessInfo processInfo];
NSEntityDescription *newShotEntity = [[mom entitiesByName] objectForKey:#"Entity"];
Entity *shEnt = [[Entity alloc] initWithEntity:newShotEntity insertIntoManagedObjectContext:moc];
shEnt.pid = [processInfo processIdentifier]; // EXC_BAD_ACCESS (code=1, address=0x28ae) here !!!
NSError *error;
if (![moc save: &error])
{
NSLog(#"Error while saving\n%#",
([error localizedDescription] != nil) ? [error localizedDescription] : #"Unknown Error");
exit(1);
}
Im really confused why I'm having this error, since when I hardcoded the Data Model instead of using .xcdatamodeld file it was working just fine!
Any kind of help is really appreciated!
EDIT 1: since I'm having all those questions asked I want to make everything clear, sorry for not providing all this before.
// Entity.h
#import <CoreData/CoreData.h>
#interface Entity : NSManagedObject
#property (strong) NSDate *date;
#property (assign) NSInteger pid;
#end
//Entity.m
#import "Entity.h"
#interface Entity ()
#property (strong) NSDate *primitiveDate;
#end
#implementation Entity
#dynamic date,primitiveDate,pid;
- (void) awakeFromInsert
{
[super awakeFromInsert];
self.primitiveDate = [NSDate date];
}
- (void)setNilValueForKey:(NSString *)key
{
if ([key isEqualToString:#"pid"]) {
self.pid = 0;
}
else {
[super setNilValueForKey:key];
}
}
#end
Using scalar values in core data is a bit more work than using the recommended NSNumber. This is described in detail in this section of the Core Data Programming Guide.
I strongly recommend you switch this property to NSNumber. Your assignment statement would then be:
shEnt.pid = [NSNumber numberWithInt:[processInfo processIdentifier]];

Something is wrong with singleton...unable adding a child because it is nil

I use a singleton the first time and I don't really know how to implement it...
Ok I need to explain some things:
In Hexagon.h (which inherits from CCNode) I want to create multiple sprites (here referred to as "hexagons"). However, they are not added to the scene yet. They are being added in the HelloWorldLayer.m class by calling Hexagon *nHex = [[Hexagon alloc]init]; . Is that correct ? Is it then iterating through the for loop and creating all hexagons or only one ?
Well anyways, I have a singleton class which has to handle all the public game state information but retrieving is not possible yet.For instance I cannot retrieve the value of existingHexagons, because it returns (null) objects. Either I set the objects wrongly or I am falsely retrieving data from the singleton. Actually, I would even appreciate an answer for one of these questions. Please help me. If something is not clear, please add a comment and I'll try to clarify it.
What I have right now is the following:
GameStateSingleton.h
#import <Foundation/Foundation.h>
#interface GameStateSingleton : NSObject{
NSMutableDictionary *existingHexagons;
}
+(GameStateSingleton*)sharedMySingleton;
-(NSMutableDictionary*)getExistingHexagons;
#property (nonatomic,retain) NSMutableDictionary *existingHexagons;
#end
GameStateSingleton.m
#import "GameStateSingleton.h"
#implementation GameStateSingleton
#synthesize existingHexagons;
static GameStateSingleton* _sharedMySingleton = nil;
+(GameStateSingleton*)sharedMySingleton
{
#synchronized([GameStateSingleton class])
{
if (!_sharedMySingleton)
[[self alloc] init];
return _sharedMySingleton;
}
return nil;
}
+(id)alloc
{
#synchronized([GameStateSingleton class])
{
NSAssert(_sharedMySingleton == nil, #"Attempted to allocate a second instance of a singleton.");
_sharedMySingleton = [super alloc];
return _sharedMySingleton;
}
return nil;
}
-(id)init {
self = [super init];
if (self != nil) {
}
return self;
}
#end
Hexagon.m
-(CCSprite *)init{
if( (self=[super init])) {
NSString *mainPath = [[NSBundle mainBundle] bundlePath];
NSString *levelConfigPlistLocation = [mainPath stringByAppendingPathComponent:#"levelconfig.plist"];
NSDictionary *levelConfig = [[NSDictionary alloc] initWithContentsOfFile:levelConfigPlistLocation];
NSString *currentLevelAsString = [NSString stringWithFormat:#"level%d", 1];
NSArray *hexPositions;
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad){
hexPositions = [[levelConfig valueForKey:currentLevelAsString] valueForKey:#"hexpositionIpad"];
}
else{
hexPositions = [[levelConfig valueForKey:currentLevelAsString] valueForKey:#"hexpositionIphone"];
}
NSString *whichType = [NSString stringWithFormat:#"glass"];
CGSize screenSize = [CCDirector sharedDirector].winSize;
if ([whichType isEqualToString:#"stone"]){
hexagon = [CCSprite spriteWithFile:#"octagonstone.png"];
}else if([whichType isEqualToString: #"glass"]){
hexagon = [CCSprite spriteWithFile:#"octagoncolored1.png"];
}else if([whichType isEqualToString: #"metal"]){
hexagon = [CCSprite spriteWithFile:#"octagonmetal.png"];
}
NSMutableDictionary *eHexagons =[[GameStateSingleton sharedMySingleton] getExistingHexagons];
for (int i=0;i < [hexPositions count];i++){
CGPoint location = CGPointFromString([hexPositions objectAtIndex:i]);
CGPoint nLocation= ccp(screenSize.width/2 + 68 * location.x,screenSize.height/2 + 39 * location.y);
NSString *aKey = [NSString stringWithFormat:#"hexagon%d",i];
hexagon =[CCSprite spriteWithFile:#"octagoncolored1.png"];
hexagon.position = nLocation;
[eHexagons setObject:hexagon forKey:aKey];
[self addChild:[eHexagons valueForKey:aKey] z:3];
[[GameStateSingleton sharedMySingleton]setExistingHexagons:eHexagons];
}
NSLog(#"these are the existinghexagons %#", existingHexagons);
//This returns a dictionary with one (null) object
}
return hexagon;
}
HelloWorldLayer.m -> -(id)init method
Hexagon *nHex = [[Hexagon alloc]init];
First of all, it returns null because the existingHexagons array has never been initialized in the first place. Go to the init function of your singleton and add:
existingHexagons = [[NSMutableArray alloc]init];
As for your For Loop question, I did not get it. I recommend making one StackOverflow question per query instead of putting two in one.

Objective-c: NSMutableDictionary setObject not working

Not sure what I am doing wrong here. When I try to check the dictionary either by a specific key or allkeys I either get an error or null. (I know I'm using a string where I could be using a boolean for the conditional I just like having a check like that say true or false instead of YES and NO. Add that to my OCD list. :D ) activePlayer is set in an awakeFromNib method to 1, it can be switched using a popupbutton between P1 and P2.
- (IBAction)setPlayer:(id)sender {
haserror = #"false";
errmsg = [NSMutableString stringWithCapacity:0];
[errmsg retain];
[errmsg appendString: #"There was a problem setting your team up\n\n"];
thisTeamName = [txtTeamName stringValue];
thisTeamColor = [pdTeamColor itemTitleAtIndex:[pdTeamColor indexOfSelectedItem]];
//validate form
if ([thisTeamName isEqualToString:#""]) {
haserror = #"true";
[errmsg appendString: #"You must enter a team name\n\n"];
}
if ([thisTeamColor isEqualToString:#"Select A Color"]) {
haserror = #"true";
[errmsg appendString: #"You must select a team color\n\n"];
}
//check for errors
if (haserror == #"true") {
[self showAlert: errmsg];
} else {
//set up treasury
treasury = 1000;
//convert to string for display
[lblTreasury setStringValue: [NSString stringWithFormat:#"$%i", treasury] ];
//add items to dictionary
if (activePlayer == #"1") {
[p1TeamData setObject:thisTeamName forKey:#"teamName"];
[p1TeamData setObject:thisTeamColor forKey:#"teamColor"];
[p1TeamData setObject:[NSString stringWithFormat:#"%i", treasury] forKey:#"cash"];
} else {
[p2TeamData setObject:thisTeamName forKey:#"teamName"];
[p2TeamData setObject:thisTeamColor forKey:#"teamColor"];
[p2TeamData setObject:[NSString stringWithFormat:#"%i", treasury] forKey:#"cash"];
}
NSLog(#"%#", [p1TeamData allKeys]);
}
[errmsg release];
}
[Edit: here's the .h file]
#interface GameController :NSObject {
IBOutlet id btnSaveData;
IBOutlet id lblTreasury;
IBOutlet id pdPickPlayer;
IBOutlet id pdTeamColor;
IBOutlet id txtTeamName;
int activePlayer;
NSString* activePlayerName;
NSString* activePlayerTeamColor;
int treasury;
NSMutableDictionary* p1TeamData;
NSMutableDictionary* p2TeamData;
NSArray* players;
NSArray* teamColors;
NSArray* unittypes;
NSString* thisTeamName;
NSString* thisTeamColor;
NSMutableString* errmsg;
NSString* haserror;
}
-(void) awakeFromNib;
- (IBAction) getPlayer : (id)sender;
- (IBAction) setPlayer : (id)sender;
-(void) showAlert : (NSMutableString* ) m;
#end
Make sure you initialize the collections in the -initXXX method. If not, they will be assigned to nil.
-(id)initXXX:... {
if ((self = [super initYYY:...])) {
...
p1TeamData = [[NSMutableDictionary alloc] init];
p2TeamData = [[NSMutableDictionary alloc] init];
...
}
return self;
}
If all you want are "true" and "false", just define them yourself. It's not a reason to use string instead of BOOL. In fact, Foundation already defined TRUE and FALSE besides YES and NO.
Also, please use an integer for activePlayer.
You should always compare NSString with -isEqualToString:, not ==.
if ([haserror isEqualToString:#"true"])
...
if ([activePlayer isEqualToString:#"1"])
This should be the reason why p1TeamData is always nil, because activePlayer == #"1" is unreliable and there could be player-1 stuff assigned to p2TeamData.