I have an NSMutableArray called selectedUsers to which I am adding objects using a method called addUser. The objects being added are most often of type PLManagedUser (a core data managed object) although sometimes the object could be a string. In any case, if the array already contains the object, I do not want to add it to the array. Here is the code:
- (void)addUser:(id)user withTitle:(NSString *)title {
if (![_selectedUsers containsObject:user]) {
[_selectedUsers addObject:user];
}
}
I have noticed that if I try to add the same user back to back using the above method, the containsObject catches it, and duplicates are not added. However, if I add the same user (with the same memory address) after having added other objects in between, the duplicate will be added.
I am printing the contents of the array each time I add something to confirm that the duplicate objects are in the array.
My question is, is there any obvious reason why containsObject isn't consistently working here?
You need to provide the ability for an object to identify itself as equal to another object of the same class, and to do this you implement the isEqual: and hash methods.
Having said that, the explanation in your question is the opposite of what I would have expected.
Related
I am a little confused to how containsObject works. Does it check to see if it contains an instance of an object type or does it compare the inside of the objects variables etc to see if they match?
This is an implementation detail, you can work on the basis that it calls isEqual: on each items and works on the result of that.
Under the hood it's probably calling hash on each item and comparing that, then, if the hash matches it will call isEqual: to make sure it's a real match.
I am working on an app where I send a query to a webpage and get the information and save it into my array. Now, all this works except for then I come to take the value from my array and display it in my text view. using this code
-(void)viewDidAppear:(BOOL)animated
{
self.tvSoilN.text = [[_soilNotes objectAtIndex:0] objectForKey:#"crop"];
}
However, I get the error of the array being empty.
Now that makes sense because using a breakpoint I can see that the code above actually runs before the code be in my model.m file which I use to write the data in the array
I cant post images but when I use a break point on the if(self.delegate) line It shows me
_locations (and when i drill down i see)
_crop_soil(this is the field in my database)=#"Blah blah blah")
which makes me realize that my data is indeed being retreived from online. But i am just having difficulty in displaying it from my array or anywhere.
Would like some help in especially where to call my array as I also think i am doing it wrong.Thanks you
IN Implementation file
-(void)itemsDownloaded:(NSArray *)items
{
// This delegate method will get called when the items are finished downloading
// Set the downloaded items to the array
_soilN = items;
NSLog(#"soil%#",_soilNotes);
self.tvSoilN.text=[[_soil objectAtIndex:0]objectForKey:#"crop"];
}
Use valueForKey not objectForKey. Got there in the end!
So long story short, there's a discrepancy between the output of a NSMutableDictionary's contents and the result of calling allValues on the same object. Below is some debugger output after inspecting the object which demonstrates my problem: (made generic of course)
(lldb) po self.someDict.allKeys
<__NSArrayI 0xa5a2e00>(
<SomeObject: 0xa5a2dc0>,
<SomeObject: 0xa5a2de0>
)
(lldb) po self.someDict.allValues
<__NSArrayI 0xa895ca0>(
0.5,
0.5
)
(lldb) po self.someDict
{
"<SomeObject: 0xa5a2dc0>" = (null);
"<SomeObject: 0xa5a2de0>" = (null);
}
So as we can see, the actual output of the NSMutableDictionary contains null values for both its entries, but the contents of .allValues contains the proper data. These three outputs were taken at the same time in execution.
I'm not sure why this is happening, but I think it may have something to do with the fact that I'm encoding/decoding the object which this dictionary is a property of using CoreData. I believe I'm doing this properly:
[aCoder encodeObject:self.someDict forKey:#"someDict"];
and to decode
self.someDict = [aDecoder decodeObjectForKey:#"someDict"];
The weird thing is that if I inspect the dictionary before it ever gets encoded, it is still in the state described at the beginning of the post, so this is why I'm doubtful the CoreData actions are screwing with the contents.
As always, please don't hesitate to request additional information.
EDIT: The problem was as answered below. I was using a custom class which didn't cooperate with isEqual, so my solution was to change the storage and structure of my logic, which made using a Dictionary unnecessary.
I have not been able to duplicate the problem using NSString as keys and NSNumber as values. I suspect that your custom class does not properly implement hash and/or isEqual. Specifically, the results from those two methods must be consistent, meaning that if isEqual returns true, then the hash values for the two objects must be identical.
You also need to ensure that your class implements NSCopying properly and that a copy is equal to the original.
As a general rule, don't use custom objects for dictionary keys. Just use strings and be done with it.
As user3386109 points out, custom objects must properly implement the -hash and -isEqual methods in order to be used as dictionary keys, and even then, custom objects don't work correctly for dictionary keys for things like key/value coding.
Problem: Need Unique Identifier
I'm saving a custom object using NSArchiver. It retains all of my objects data, however, everytime I archive and unarchive it gives them new addresses
"<Item: 0x17005d070>",
"<Item: 0x17005e4b0>",
"<Item: 0x17005e4e0>"
"<Item: 0x170059fe0>",
"<Item: 0x170059ec0>",
"<Item: 0x17005a0a0>"
For the same 3 objects.
This causes problems because I need to hold a copy of some of the items and persist this copy, and later compare the copy to the original for equality [currentItem isEqual:oldItem]; Even when this should return true (i.e. the object is the same in terms of name, location, etc.) it will return false because the pointers are different. What's the solution to this? I've thought about adding a uniqueID to each object and then just storing that uniqueID, but that seems like overkill for what I'm trying to do.
Solution:
You can't rely on an objects memory address as a unique identifier. Use NSUUID.
The problem is that you have not implemented isEqual: for your Item class. Implement it. Otherwise, as you've observed, we fall back on NSObject's definition of isEqual:, which is identicality (i.e. these are one and the same object). It is up to you to tell Cocoa that an Item should return true if the object is the same in terms of name, location, etc.; it doesn't magically know this.
I suspect the solution to what I'm trying to do is fairly straight forward--yet, I'm unable to get it working myself.
Here's what I'm trying to do: I've built a card game that is initialized with the cards when the game loads. Now, some of those cards have certain properties (card type, card name, as well as a special array). The card type and card name objects are fairly easy to retrieve since they're just one object (and I can call using objectatindex). However, the special array contains several keywords that fluctuate depending on which card is chosen. So instead of initializing these keywords one by one (like I did for card type and card name), I put them into their own special array.. or an array within an array. Here's my code:
itemObjects class:
#synthesize cardName=_cardName;
#synthesize cardType=_cardType;
-(id) initWithCardName:(NSString*)cardName initWithCardType:(NSString*)cardType initWithSpecialArray:(NSArray*) specialArray{
self=[super init];
if (self){
_cardName=cardName;
_cardType=cardType;
}
return self;
}
model class
-(NSMutableArray*)deck{
if (_deck==nil){
_deck=[[NSMutableArray alloc]initWithObjects:
[[itemObjects alloc]initWithCardName:#"The Long Way" initWithCardType:#"bill" initWithSpecialArray:[[NSArray alloc]initWithObjects:#"fast", #"high", nil]],
[[itemObjects alloc]initWithCardName:#"A Short Cut" initWithCardType:#"bill" initWithTrendArray:[[NSArray alloc]initWithObjects:#"small", #"tall", nil]],nil];
View Controller class (this is where I'm trying to call one of the objects, "fast" for example, but with no success
NSString* testing=[[[self.model.deck objectAtIndex:indexPath.row]arrayForKey:#"specialArray"]objectAtIndex:0];
NSLog(#"%#",testing);
I believe I've initialized my "specialArray" correctly and the issue is with how I'm attempting to call it but if I've made a mistake there, any advice would be much appreciated. Thanks!
EDIT: This particular issue has been solved thanks to WendiKidd. It turned out that I wasn't initializing my specialArray correctly. This has led to a separate issue which I have linked to here. I've also posted my corrected code below for those interested in the future:
#synthesize cardName=_cardName;
#synthesize cardType=_cardType;
#synthesize specialArray=_specialArray;
-(id) initWithCardName:(NSString*)cardName initWithCardType:(NSString*)cardType initWithSpecialArray:(NSArray*) specialArray{
self=[super init];
if (self){
_cardName=cardName;
_cardType=cardType;
_specialArray=specialArray;
}
return self;
}
You aren't initializing your special array at all. You pass it to initWithCardName, but you're never setting it to anything. You need to store the special array inside that class, just like you do with cardName and cardType.
Secondly, I can't make heads or tails of the line where you're trying to access the special array. You haven't given us the proper information to see where the object is being stored to tell if you're even properly accessing a card object, but I definitely don't see where you've ever used the key #"specialArray" before, so there's no reason to expect that will return anything. However the rest of your data structure works, at some point you're going to have an object of whatever class the initWithCardName function initializes (for example purposes I'm going to go ahead and call it CardObject). To get the info from the special array, you're going to have to save an object called specialArray into the CardObject class, as already mentioned. Then you can write something like this:
CardObject* card = //[whatever you have to do to access the right card object]
for(NSString* specialAttribute in card.specialArray)
{
NSLog(#"Special Attribute: %#", specialAttribute);
}
And that should print all the special attributes of the card quite nicely.
If you really do just want the first item in the list, as your example was trying to access, this should work just fine:
CardObject* card = //[whatever you have to do to access the right card object]
NSLog(#"Special Attribute: %#", [card.specialArray objectAtIndex:0]);