Unique keys in NSDictionary - how to check given keys are copies? - objective-c

I have an NSDictionary in which I use my own classes (NSObject subclasses) as keys and would like to make sure that I do not include the same key twice. However, because NSDictionary copies its keys, if I try to check whether an object is in the dictionary, it never thinks it is.
For example,
MyClass* obj = [[MyClass alloc] init];
NSMutableDictionary* dict = [NSMutableDictionary dictionary];
[dict setObject:someObj forKey:obj];
if ([[dict allKeys] contains:obj]) // always returns false
// obj is already in dict
else
// add obj to dict etc.
Similarly, if I want to change the object associated with this key, it seems to create a new entry.
// dict is empty
// say [obj description] gives 'MyClass : 0x1' - (impossible address?)
[dict setObject:someObj forKey:obj];
// dict: { 'MyClass : 0x2' = someObjDesc }
[dict setObject:someOtherObj forKey:obj];
// dict: { 'MyClass : 0x2' = someObjDesc , 'MyClass : 0x3' = someOtherObjDesc }
Also, this same thing leads to not being able to access the items in the dictionary from the original object
[dict setObject:someObj forKey:obj];
[dict objectForKey:obj]; // returns null
So, as far as the uniqueness is concerned, would I be best off keeping track of the keys in a separate array or is there a better way of doing this.
I considered implementing an isEqual method based on a unique variable (such as a name) but didn't think that was the Right Thing to do.
Background (in case it turns out that maybe I'm just using the wrong thing entirely):
I want to keep track of information about a group of people going to different places. So each person at each place has some info. What I've done is used nested dictionaries so the key to the main dictionary is a Person object and the object another dictionary. This latter dictionary has key Place and info as the object. I think this is Java syntax but something like > (the array holds the info). I want to be able to add a Person only if the don't already exist, add a Place (for each person), change the array.
Any help on any of this would be greatly appreciated!

You should always use NSStrings as keys for dictionaries, especially if you are new at objective-C. There are a few things that I can see you are doing wrong with your current implementation - you would need to read up on key requirements for NSDictionaries.
You can do what you want with strings as keys - person's name, etc.
The objects in a dictionary have all the info about a certain person:
NSDictionary* personsInfo = [mainDict objectForKey:#"Jane Smith"];
NSString* addressLine1 = [personsInfo objectForKey#"addressLine1"];
--Tom

The simple answer would be to make it so that the MyClass doesn't actually copy anything.
That would be something like changing:
- (id) copyWithZone:(NSZone *)zone {
MyClass * foo = (MyClass *)[super copyWithZone:zone];
[foo configureCopy];
return foo;
}
To:
- (id) copyWithZone:(NSZone *)zone {
return [self retain];
}

Related

Cocoa class to reference many entries by a single key

I need a Cocoa data structure to keep something like a list of locations of a set of insects in a field over time. Clearly an NSDictionary has to have unique keys, so I couldn't use the insect as a key and MKUserlocation as the object.
Is there any other class besides NSArray where one does not have to step through, if/else-ing at each index?
There seems to be such a thing as NSList discussed in various places but nothing in the Apple docs.
I also saw a reference to NSIndexSet one uses in conjunction to an NSArray, but again reading the Apple docs suggests it's not useful to me.
Since Cocoa framework does not offer a multimap container, you could implement your own by creating an NSMutableDictionary, and populating it with NSMutableArray objects.
NSMutableDictionary locByInsect = [NSMutableDictionary dictionary];
// Adding a location for an insect:
MyInsect *insect = ... // Don't forget to provide hash code and equality checks
MyLocation *location = ...
NSMutableDictionary *insectLocations = locByInsect[insect];
if (insectLocations == nil) {
insectLocations = [NSMutableArray array];
locByInsect[insect] = insectLocations;
}
[insectLocations addObject:location];
// Retrieving all locations for an insect
for (MyLocation *loc in locByInsect[insect]) {
NSLog(#"Saw %# at %#", insect, loc);
}
For each insect have a key in an NSMutableDictionary. Make each key's object an NSMutableArray of locations.

Use of #synthesize and value class type checking

This is probably a long shot, but I've got objects with a lot of properties. The values of these objects are populated from NSDictionary's created from a database request. Because of this, there may be NSNull values contained in those NSDictionaries that will automatically get assigned to the properties. I need the properties to automatically discard values/objects that aren't of the correct type. Currently I do it like this:
- (void) setViewID:(NSString *)viewID{
if (!viewID || [viewID isKindOfClass:[NSString class]]) _viewID = viewID;
}
But that ends up being a lot of extra code when I've got 30-50 properties. Is there a way to synthesize this behavior? It seems like it would be a common enough requirement, but I can't seem to find a way to do it aside from writing it all out.
Why not check for NSNull when you are going through the dictionary? E.g.
for (NSString *key in dictionary) {
id value = [dictionary objectForKey:key];
if (value == [NSNull null]) {
value = nil;
}
[self setValue:value forKey:key];
}

Category NSMutableDictionary/NSDictionary should return NSMutableDictionary/NSDictionary Depending of the caller class

I was busy with this for some hours, but see no head or tail.
How should I create an NS(Mutable)Dictionary category that I can let return the same class as the sending method class.
Sample
+(NSDictionary *)dictionaryWithAdditions:path error:(NSError**)error;
NSDictionary * dict = [NSDictionary dictionaryWithAdditions:path error:&error];
should return an NSDictionary.
AND
+(NSMutableDictionary *)dictionaryWithAdditions:path error:(NSError**)error;
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithAdditions:path error:&error];
should return an NSMutableDictionary.
I dont want to return an id, to avoid any possible warning etc.
Suggestions how I should create this in one category with error management?
You either need to create two different methods (e.g. dictionaryWithAdditions: and mutableDictionaryWithAdditions:) or return id (I would suggest the latter). The type system doesn't allow us to say "NSDictionary if the receiver is statically typed as NSDictionary, NSMutableDictionary if the receiver is statically typed as NSMutableDictionary, and error otherwise".
What warnings would you get when returning id? That's what the standard init method does anyway.
Your method would probably look something like:
+ (id)dictionaryWithAdditions:path error:(NSError**)error;
{
id dictionary = [[[self alloc] init] autorelease];
// ... do something
return dictionary;
}

Getting an object from an NSSet

If you can't get an object with objectAtIndex: from an NSSet then how do you retrieve objects?
There are several use cases for a set. You could enumerate through (e.g. with enumerateObjectsUsingBlock or NSFastEnumeration), call containsObject to test for membership, use anyObject to get a member (not random), or convert it to an array (in no particular order) with allObjects.
A set is appropriate when you don't want duplicates, don't care about order, and want fast membership testing.
NSSet doesn't have a method objectAtIndex:
Try calling allObjects which returns an NSArray of all the objects.
it is possible to use filteredSetUsingPredicate if you have some kind of unique identifier to select the object you need.
First create the predicate (assuming your unique id in the object is called "identifier" and it is an NSString):
NSPredicate *myPredicate = [NSPredicate predicateWithFormat:#"identifier == %#", identifier];
And then choose the object using the predicate:
NSObject *myChosenObject = [mySet filteredSetUsingPredicate:myPredicate].anyObject;
NSArray *myArray = [myNSSet allObjects];
MyObject *object = [myArray objectAtIndex:(NSUInteger *)]
replace NSUInteger with the index of your desired object.
For Swift3 & iOS10 :
//your current set
let mySet : NSSet
//targetted index
let index : Int
//get object in set at index
let object = mySet.allObjects[index]
NSSet uses the method isEqual: (which the objects you put into that set must override, in addition, the hash method) to determine if an object is inside of it.
So, for example if you have a data model that defines its uniqueness by an id value (say the property is:
#property NSUInteger objectID;
then you'd implement isEqual: as
- (BOOL)isEqual:(id)object
{
return (self.objectID == [object objectID]);
}
and you could implement hash:
- (NSUInteger)hash
{
return self.objectID; // to be honest, I just do what Apple tells me to here
// because I've forgotten how Sets are implemented under the hood
}
Then, you can get an object with that ID (as well as check for whether it's in the NSSet) with:
MyObject *testObject = [[MyObject alloc] init];
testObject.objectID = 5; // for example.
// I presume your object has more properties which you don't need to set here
// because it's objectID that defines uniqueness (see isEqual: above)
MyObject *existingObject = [mySet member: testObject];
// now you've either got it or existingObject is nil
But yeah, the only way to get something out of a NSSet is by considering that which defines its uniqueness in the first place.
I haven't tested what's faster, but I avoid using enumeration because that might be linear whereas using the member: method would be much faster. That's one of the reasons to prefer the use of NSSet instead of NSArray.
for (id currentElement in mySet)
{
// ** some actions with currentElement
}
Most of the time you don't care about getting one particular object from a set. You care about testing to see if a set contains an object. That's what sets are good for. When you want to see if an object is in a collection sets are much faster than arrays.
If you don't care about which object you get, use -anyObject which just gives you one object from the set, like putting your hand in a bag and grabbing something.
Dog *aDog = [dogs anyObject]; // dogs is an NSSet of Dog objects
If you care about what object you get, use -member which gives you back the object, or nil if it's not in the set. You need to already have the object before you call it.
Dog *spot = [Dog dogWithName:#"Spot"];
// ...
Dog *aDog = [dogs member:spot]; // Returns the same object as above
Here's some code you can run in Xcode to understand more
NSString *one = #"One";
NSString *two = #"Two";
NSString *three = #"Three";
NSSet *set = [NSSet setWithObjects:one, two, three, nil];
// Can't use Objective-C literals to create a set.
// Incompatible pointer types initializing 'NSSet *' with an expression of type 'NSArray *'
// NSSet *set = #[one, two, three];
NSLog(#"Set: %#", set);
// Prints looking just like an array but is actually not in any order
//Set: {(
// One,
// Two,
// Three
// )}
// Get a random object
NSString *random = [set anyObject];
NSLog(#"Random: %#", random); // Random: One
// Iterate through objects. Again, although it prints in order, the order is a lie
for (NSString *aString in set) {
NSLog(#"A String: %#", aString);
}
// Get an array from the set
NSArray *array = [set allObjects];
NSLog(#"Array: %#", array);
// Check for an object
if ([set containsObject:two]) {
NSLog(#"Set contains two");
}
// Check whether a set contains an object and return that object if it does (nil if not)
NSString *aTwo = [set member:two];
if (aTwo) {
NSLog(#"Set contains: %#", aTwo);
}

NSMutableDictionary error

I want to use NSMutableDictionary to cache some data i will use later. My custom object is following:
#interface MyData : NSObject {
NSRange range;
NSMutableArray *values;
}
#property (nonatomic, retain) NSMutableArray *values;
and implement:
- (id)init {
if (self = [super init]) {
values = [[NSMutableArray alloc] init];
}
return self;
}
and when i wanna cache it, i use it like this:
NSMutableDictionary *cache = [[NSMutableDictionary alloc] init];
NSString *key = #"KEY";
MyData *data = [[MyData alloc] init];
// save some data into data
[data.values addObject:"DATA1"];
[data.values addObject:"DATA2"];
//... ...
[cache setObject:data forKey:key];
My questions is the count of cache.values is zero when i retrieve this object later as follow:
[cache objectForKey:#"KEY"];
i can retrieve "data" and the object's memory address is the same as the address when i put it into cache.
what's wrong? i need some kind guys help, any info is helpful. thanks
As Carl Norum pointed out, you're passing C strings to addObject:. addObject:, as its name suggests, requires a pointer to a Cocoa object; a C string is a pointer to characters. You need to pass NSString objects there; for literal strings, this simply requires prefixing them with #: "Fred" is a constant C string, whereas #"Fred" is a constant NSString object.
Is cache an instance variable? It looks like it's not; it appears to be a local variable, which means you're creating a new dictionary object every time. That's why there's nothing you've added previously (to previous dictionaries) in the new one. It also means you're leaking those previous dictionaries, since you're not releasing them (not in the code you showed, anyway).
Make cache an instance variable and only create the dictionary when you don't already have one (i.e., when cache == nil). Creating the dictionary in your init method is one good way. And make sure you manage its lifetime appropriately, so you don't leak and/or crash.
First of all your objects your adding don't look right it should have an # before the string. Like #"DATA1"
Second when you add an object to a dictionary or an array it does not make an actual copy of it. It just creates a pointer to it so if those objects are destroyed or moved somewhere also they are also gone out of your dictionary. A better way to make a cache of your values would be to copy the objects like so:
MyData* cache = [[MyData alloc] init];
for (int i = 0; i < [data.values count]; i ++){{
[cache.values addObject:[NSString stringWithString:[data.values objectAtIndex:i]]];
}
Don't use a dictionary in this situation.