Why does [NSSet containsObject] fail for SKNode members in iOS8? - ios8

Two objects are added to an NSSet, but when I check membership, I can't find one of them.
The test code below worked fine in iOS7 but fails in iOS8.
SKNode *changingNode = [SKNode node];
SKNode *unchangingNode = [SKNode node];
NSSet *nodes = [NSSet setWithObjects:unchangingNode, changingNode, nil];
changingNode.position = CGPointMake(1.0f, 1.0f);
if ([nodes containsObject:changingNode]) {
printf("found node\n");
} else {
printf("could not find node\n");
}
Output:
could not find node
What happened between iOS7 and iOS8, and how can I fix it?

SKNode's implementations of isEqual and hash have changed in iOS8 to include data members of the object (and not just the memory address of the object).
The Apple documentation for collections warns about this exact situation:
If mutable objects are stored in a set, either the hash method of the
objects shouldn’t depend on the internal state of the mutable objects
or the mutable objects shouldn’t be modified while they’re in the set.
For example, a mutable dictionary can be put in a set, but you must
not change it while it is in there.
And, more directly, here:
Storing mutable objects in collection objects can cause problems.
Certain collections can become invalid or even corrupt if objects they
contain mutate because, by mutating, these objects can affect the way
they are placed in the collection.
The general situation is described in other questions in detail. However, I'll repeat the explanation for the SKNode example, hoping it helps those who discovered this problem with the upgrade to iOS8.
In the example, the SKNode object changingNode is inserted into the NSSet (implemented using a hash table). The hash value of the object is computed, and it is assigned a bucket in the hash table: let's say bucket 1.
SKNode *changingNode = [SKNode node];
SKNode *unchangingNode = [SKNode node];
printf("pointer %lx hash %lu\n", (uintptr_t)changingNode, (unsigned long)changingNode.hash);
NSSet *nodes = [NSSet setWithObjects:unchangingNode, changingNode, nil];
Output:
pointer 790756a0 hash 838599421
Then changingNode is modified. The modification results in a change to the object's hash value. (In iOS7, changing the object like this did not change its hash value.)
changingNode.position = CGPointMake(1.0f, 1.0f);
printf("pointer %lx hash %lu\n", (uintptr_t)changingNode, (unsigned long)changingNode.hash);
Output:
pointer 790756a0 hash 3025143289
Now when containsObject is called, the computed hash value is (likely) assigned to a different bucket: say bucket 2. All objects in bucket 2 are compared to the test object using isEqual, but of course all return NO.
In a real-life example, the modification to changedObject probably happens elsewhere. If you try to debug at the location of the containsObject call, you might be confused to find that the collection contains an object with the exact same address and hash value as the lookup object, and yet the lookup fails.
Alternate Implementations (each with their own set of problems)
Only use unchanging objects in collections.
Only put objects in collections when you have complete control, now
and forever, over their implementations of isEqual and hash.
Track a set of (non-retained) pointers rather than a set of objects: [NSSet setWithObject:[NSValue valueWithPointer:(void *)changingNode]]
Use a different collection. For instance, NSArray will be affected by changes to
isEqual but won't be affected by changes to hash. (Of course, if
you try to keep the array sorted for quicker lookup, you'll have
similar problems.)
Often this is the best alternative for my real-world situations: Use an NSDictionary where the key is the [NSValue valueWithPointer] and the object is the retained pointer. This gives me: quick lookup of an object that will be valid even if the object changes; quick deletion; and retention of objects put in the collection.
Similar to the last, with different semantics and some other useful options: Use an NSMapTable with option NSMapTableObjectPointerPersonality so that key objects are treated as pointers for hashing and equality.

Related

Can I use the [NSArray containsObject:] function on attributes of objects?

Background Information:
I program an iOS application that contains 2 galleries: A local gallery and a server gallery. When the user updates the server gallery and merges it into his local one, the app should only download the new images.
To minimize memory consumption I save the images and fill the arrays with instances of an ImageEntity class with following attributes: fileName, filePath & votingStatus.
I tried to use following logic to check if the image already exists:
for (ImageEntity *imageEntity in self.serverImagesArray) {
if (![self.localImagesArray containsObject:imageEntity]){
[self.localImagesArray addObject:imageEntity];
}
}
But because each entity is a separate object it will always be added. Each entity has a unique fileName though.
Question:
Can I somehow extend the [NSArray containsObject:] function to check if one object in the array has an attribute equal to "someValue"? (When I use Cocoa-Bindings in combination with an ArrayController, I can assign attributes of array elements - I would like to access the attributes similar to this).
I know that I could use compare each entity of the local array to each element on the server array. I would have to do O(n^2) comparisons though and the gallery may contain several hundred images.
Bonus question: Am I already doing this without realizing it? Does anybody have details about Apple's implementation of this function? Is there some fancy implementation or are they just iterating over the array comparing every element?
The way I do it is use valueForKey: in combination with containsObject:. So in your case you should collect all file names of the array and then check if the array contains specific file name you need:
NSArray * fileNames = [fileEntityObjects valueForKey:#"fileName"];
BOOL contains = [fileNames containsObject:#"someFilename.jpg"];
This would work if fileName is property of every object in fileEntityObjects array.
Update
Yes, you can do this with NSPredicate as well:
NSPredicate * predicate = [NSPredicate predicateWithFormat:#"SELF.fileName = %#", "someFileName.jpg"];
NSArray * filteredArray = [fileEntityObjects filteredArrayUsingPredicate:predicate];
Note though that instead of boolean you'd get an array of objects having that filename.
As this question is tagged "performance" I'm adding another answer:
To avoid the n^2 comparisons we have to find a faster way to detect images that are already present. To do this we use a set that can perform lookups very quickly:
NSMutableSet *localFileNames = [NSMutableSet set];
for (ImageEntity *imageEntity in self.localImagesArray)
[localFileNames addObject:imageEntity.fileName];
Then we iterate over the server images as before. The previous containsObject: is replaced by the fast set lookup:
for (ImageEntity *imageEntity in self.serverImagesArray) {
if ([localFileNames member:imageEntity.fileName] == nil)
[self.localImagesArray addObject:imageEntity];
}

Objective-C EXC_BAD_ACCESS

Ok so I've recently decided to try to teach myself Objective-C (I'm interested in iPhone development), however I've never used C or any of its derivatives before, and as such am running into some problems.
I decided to start out by writing a very basic card application that creates a deck of cards, shuffles the deck, and then displays the cards on the screen using UIButtons, however I'm having a problem with my shuffling algorithm. Every time it gets called I get an EXC_BAD_ACCESS error, which I know means there's something desperately wrong with my code, but I just can't figure out what it is.
- (void) randomize {
NSMutableArray *tmpDeck = [[NSMutableArray alloc] init];
for(Card *tmp in _cards) {
BOOL didInsert = NO;
while(!didInsert) {
NSUInteger random = arc4random_uniform(54);
if([[tmpDeck objectAtIndex:random] isEqual:nil]) {
[tmpDeck insertObject:tmp atIndex:random];
didInsert = YES;
}
}
}
_cards = tmpDeck;
_hasBeenRandomized = YES;
}
_cards is a pointer to an NSMutableArray containing the unshuffled deck of card objects, and _hasBeenRandomized is a boolean (obviously) that keeps track of whether or not the deck has been randomized.
I've tried to use the debugger to work out what exactly is going on here, but I can't even step into the method without my program crashing. This leads me to believe that the problem has to come from the very first line, but it's just a straightforward creation of an NSMutableArray, so I don't know how it could be that. This method is being called from within viewDidLoad. This is the entirety of the viewDidLoad method currently.
- (void)viewDidLoad
{
[super viewDidLoad];
_deck = [[Deck alloc] init];
[_deck randomize];
}
Any and all help will be appreciated. Sorry if the answer is dead obvious.
This is because you are trying to insert into an index that doesn't exist yet. You need to initialize the array with as many places in the array as you need for your cards. Either that or use a NSMutableDictionary and just insert the object with the index being the key.
To add another note, calling initWithCapacity on the array wouldn't solve this for you either since this just gives a "hint" at the size. You need the count property of the array to actually be at least as large as the index you are trying to insert. If you wanted to do an array, then you would first need to populate something in each index first. You could define this in the new array literal format or use a for loop that loops the number of times you need (your max index) and insert a dummy object in it's place.
for (int i=0; i< _cards.count; ++i)
{
[tmpDeck insertObject:#"dummy" atIndex:i];
}
Then instead of checking for 'nil' before you replace, you check if it is equal to the dummy object you inserted. This would give you an array that you can insert into any of these indexes. I personally would still probably store them in an NSMutableDictionary. But if you need it in an array for some other purpose then this is a way to do it.
You also will need to be sure to replace the object instead of inserting, otherwise you will just keep adding indexes.
[tmpDeck replaceObjectAtIndex:random withObject:tmp];
If you still get the same error, set a breakpoint in your debugger and check what the random number is and what the count of your array is. If your random number is ever greater than your array count, then you will get this error.

NSDictionary sets values for NSMutableDictionary

I'm trying to make an rpg style game. I would like to know if and how to make an NSDictionary set the value for a mutable dictionary. The characters in my game will learn moves at different levels and the user can decide which move to use. If more info is needed let me now.
Here's an example
the NSDictionary pulls from a plist of attacks.
the attacks have names and power values.
the mutable array will set its attacks to the attacks in the NSDictionary.
You're looking for NSDictionary's - (NSArray *)objectsForKeys:(NSArray *)keys notFoundMarker:(id)anObject
Which
Returns the set of objects from the dictionary that corresponds to the specified keys as an NSArray.
Parameters keys An NSArray containing the keys for which to return corresponding values.
anObject The marker object to place in the corresponding element of the returned
array if an object isn’t found in the dictionary to correspond to a given key.
Discussion The objects in the returned array and the keys array have a one-for-one correspondence, so that the nth object in the returned array corresponds to the nth key in keys.
For example, you could run [dict objectsForKeys:[NSArray arrayWithObjects:str1, str2, nil] notFountMarker:[NSNull null]]; which will return the objects for the keys stored in str1, and str2. If the keys aren't found, the place holder will be an NSNull object.
If you're just after a mutable copy of a dictionary . . .
NSDictionary *atacks = /* Your dictionary of attacks */
NSMutableDictionary *mutable = [NSMutableDictionary dictionaryWithDictionary:attacks];

Attempt to insert nil

Seems like it should be easy to add a boolean to an NSMutableArray.
Assume toDoArray is intialized as an NSMutableArray. The following:
BOOL checkBoxState = NO;
[toDoArray addObject:checkBoxState];
Generates the error "attempt to insert nil."
What's the correct way to add a negative boolean to a mutable array?
As others have said, NSMutableArray can only contain Objective-C objects. They do not have to be subclasses of NSObject, but that is the most typical.
However, long before you ever see the attempt to insert nil. runtime error, you should have seen a compiler warning:
warning: passing argument 1 of 'addObject:' makes pointer from integer without a cast
It is [in a vague and roundabout way] telling you exactly what the problem is; you are trying to stick something into an array that is not a pointer [to an object].
Pay attention to warnings and fix them. Most of the time, the presence of a warning will indicate a runtime error or crash.
NSMutable arrays require an id, a weird part of Objective C. An id is any object, but not a primitive (For example, ints are primitives, while NSArrays are objects, and in extension, ids).
This question might help.
You need using NSNumber to wrap any primitive types (BOOL, int, NSInterger, etc.) before placing it inside collection object (NSArray, NSDictionary, etc.).
Add BOOL to array:
BOOL checkBoxState = NO;
NSNumber* n = [NSNumber numberWithBool:checkBoxState];
[toDoArray addObject:n];
Get BOOL from array:
NSNumber* n = [toDoArray objectAtIndex:0];
BOOL checkBoxState = [n boolValue];

How do copy and mutableCopy apply to NSArray and NSMutableArray?

What is the difference between copy and mutableCopy when used on either an NSArray or an NSMutableArray?
This is my understanding; is it correct?
// ** NSArray **
NSArray *myArray_imu = [NSArray arrayWithObjects:#"abc", #"def", nil];
// No copy, increments retain count, result is immutable
NSArray *myArray_imuCopy = [myArray_imu copy];
// Copys object, result is mutable
NSArray *myArray_imuMuta = [myArray_imu mutableCopy];
// Both must be released later
// ** NSMutableArray **
NSMutableArray *myArray_mut = [NSMutableArray arrayWithObjects:#"A", #"B", nil];
// Copys object, result is immutable
NSMutableArray *myArray_mutCopy = [myArray_mut copy];
// Copys object, result is mutable
NSMutableArray *myArray_mutMuta = [myArray_mut mutableCopy];
// Both must be released later
copy and mutableCopy are defined in different protocols (NSCopying and NSMutableCopying, respectively), and NSArray conforms to both. mutableCopy is defined for NSArray (not just NSMutableArray) and allows you to make a mutable copy of an originally immutable array:
// create an immutable array
NSArray *arr = [NSArray arrayWithObjects: #"one", #"two", #"three", nil ];
// create a mutable copy, and mutate it
NSMutableArray *mut = [arr mutableCopy];
[mut removeObject: #"one"];
Summary:
you can depend on the result of mutableCopy to be mutable, regardless of the original type. In the case of arrays, the result should be an NSMutableArray.
you cannot depend on the result of copy to be mutable! copying an NSMutableArray may return an NSMutableArray, since that's the original class, but copying any arbitrary NSArray instance would not.
Edit: re-read your original code in light of Mark Bessey's answer. When you create a copy of your array, of course you can still modify the original regardless of what you do with the copy. copy vs mutableCopy affects whether the new array is mutable.
Edit 2: Fixed my (false) assumption that NSMutableArray -copy would return an NSMutableArray.
I think you must have misinterpreted how copy and mutableCopy work. In your first example, myArray_COPY is an immutable copy of myArray. Having made the copy, you can manipulate the contents of the original myArray, and not affect the contents of myArray_COPY.
In the second example, you create a mutable copy of myArray, which means that you can modify either copy of the array, without affecting the other.
If I change the first example to try to insert/remove objects from myArray_COPY, it fails, just as you'd expect.
Perhaps thinking about a typical use-case would help. It's often the case that you might write a method that takes an NSArray * parameter, and basically stores it for later use. You could do this this way:
- (void) doStuffLaterWith: (NSArray *) objects {
myObjects=[objects retain];
}
...but then you have the problem that the method can be called with an NSMutableArray as the argument. The code that created the array may manipulate it between when the doStuffLaterWith: method is called, and when you later need to use the value. In a multi-threaded app, the contents of the array could even be changed while you're iterating over it, which can cause some interesting bugs.
If you instead do this:
- (void) doStuffLaterWith: (NSArray *) objects {
myObjects=[objects copy];
}
..then the copy creates a snapshot of the contents of the array at the time the method is called.
The "copy" method returns the object created by implementing NSCopying protocols copyWithZone:
If you send NSString a copy message:
NSString* myString;
NSString* newString = [myString copy];
The return value will be an NSString (not mutable)
The mutableCopy method returns the object created by implementing NSMutableCopying protocol's mutableCopyWithZone:
By sending:
NSString* myString;
NSMutableString* newString = [myString mutableCopy];
The return value WILL be mutable.
In all cases, the object must implement the protocol, signifying it will create the new copy object and return it to you.
In the case of NSArray there is an extra level of complexity regarding shallow and deep copying.
A shallow copy of an NSArray will only copy the references to the objects of the original array and place them into the new array.
The result being that:
NSArray* myArray;
NSMutableArray* anotherArray = [myArray mutableCopy];
[[anotherArray objectAtIndex:0] doSomething];
Will also affect the object at index 0 in the original array.
A deep copy will actually copy the individual objects contained in the array. This done by sending each individual object the "copyWithZone:" message.
NSArray* myArray;
NSMutableArray* anotherArray = [[NSMutableArray alloc] initWithArray:myArray
copyItems:YES];
Edited to remove my wrong assumption about mutable object copying
NSMutableArray* anotherArray = [[NSMutableArray alloc] initWithArray:oldArray
copyItems:YES];
will create anotherArray which is a copy of oldArray to 2 levels deep. If an object of oldArray is an Array. Which is generally the case in most applications.
Well if we need a True Deep Copy we could use,
NSArray* trueDeepCopyArray = [NSKeyedUnarchiver unarchiveObjectWithData:
[NSKeyedArchiver archivedDataWithRootObject: oldArray]];
This would ensure that all levels are actually copied retaining the mutability of the original object at each level.
Robert Clarence D'Almeida,
Bangalore, India.
You're calling addObject and removeObjectAtIndex on the original array, rather than the new copy of it you've made. Calling copy vs mutableCopy only effects the mutability of the new copy of the object, not the original object.
To state it simply,
copy returns an immutable (can't be modified) copy of the array,
mutableCopy returns a mutable (can be modified) copy of the array.
Copy (in both cases) means that you get a new array "populated" with object references to the original array (i.e. the same (original) objects are referenced in the copies.
If you add new objects to the mutableCopy, then they are unique to the mutableCopy. If you remove objects from the mutableCopy, they are removed from the original array.
Think of the copy in both cases, as a snapshot in time of the original array at the time the copy was created.
Assume
NSArray *A = xxx; // A with three NSDictionary objects
NSMutableArray *B = [A mutableCopy];
B's content is NSDictionary object not NSMutableDictionary, is it right?
-(id)copy always returns a immutable one & -(id)mutableCopy always returns a mutable object,that's it.
You have to know the return type of these copying stuff and while declaring the new object which one will be assigned the return value must be of immutable or mutable one, otherwise compiler will show you error.
The object which has been copied can not be modified using the new one,they are totally two different objects now.

Resources