2

I've read every similar question, but have determined either I'm doing something stupid (possible) or I fail to grasp the NSArray method containsObject:

I'm trying to setup a UITableView that contains saved "favorites"; locations that are kept as a custom class called "MapAnnotations." This contains stuff like coordinates, title, an info field, and a couple of other parameters. I'm successfully saving/retrieving it from a NSUserDefaults instance, but can't seem to successfully detect duplicate objects held in my NSMutableArray.

Here's the relevant code:

-(void)doSetUp
{
//load up saved locations, if it exists

NSUserDefaults *myDefaults = [NSUserDefaults standardUserDefaults];

//if there are saved locations
if ([myDefaults objectForKey:@"savedLocations"]) {

    NSLog(@"file exists!");

      //get saved data and put in a temporary array
    NSData *theData = [myDefaults dataForKey:@"savedLocations"];
      //my custom object uses NSCode protocol
    NSArray *temp = (NSArray *)[NSKeyedUnarchiver unarchiveObjectWithData:theData];
    NSLog(@"temp contains:%@",temp);
      //_myFavs currently exists as a NSMutableArray property
    _myFavs = [temp mutableCopy];

}else{

    NSLog(@"File doesn't exist");
    _myFavs = [[NSMutableArray alloc]init];
}

    //_currLoc is an instance of my Mapnnotations custom class
        // which contains coordinates, title, info, etc.

if (_currLoc != nil) {

        //if this is a duplicate of a saved location

    if ([_myFavs containsObject:_currLoc]) {

        //pop an alert

        UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@"Sorry..." message:@"That location has already been saved." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil, nil];
        [alert show];
    }else{
            //add location to end of myFavs array
        [_myFavs addObject:_currLoc];

        NSLog(@"myFavs now contains:%@",_myFavs);

        //write defaults

        NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:_myFavs];
        [myDefaults setObject:encodedObject forKey:@"savedLocations"];
        [myDefaults synchronize];
        }
    }
}

I've tried enumerating through the _myFavs array, checking for matches on specific fields (get errors for enumerating through something mutable), tried to copy to a straight array... tried to use indexOfObject:..

Jeroen Vannevel
  • 43,651
  • 22
  • 107
  • 170
james Burns
  • 864
  • 2
  • 9
  • 22

2 Answers2

5

You can use containsObject: method with custom objects that implement isEqual: method. Adding an implementation of this method to your Mapnnotations class will fix the problem:

// In the .h file:
@interface Mapnnotations : NSObject
-(BOOL)isEqual:(id)otherObj;
...
@end

// In the .m file:
@implementation Mapnnotations
-(BOOL)isEqual:(id)otherObj {
    ... // Check if other is Mapnnotations, and compare the other instance
        // to this instance
    Mapnnotations *other = (Mapnnotations*)otherObj;
    // Instead of comparing unique identifiers, you could compare
    // a combination of other custom properties of your objects:
    return self.uniqueIdentifier == other.uniqueIdentifier;
}
@end

Note: when you implement your own isEqual: method, it is a good idea to implement the hash method as well. This would let you use the custom objects in hash sets and as NSDictionary keys.

Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
  • I'm fairly sure that I will always be comparing 2 MapAnnotations. Should I do a comparison of representative properties (say... the title?) looking at the method signature you provided in your answer, am a little baffled how I determine equality with only one parameter. Would I use self.title == other.title? Sorry... am dumb designer only pretending to be a programmer. – james Burns Mar 04 '14 at 18:44
  • @jamesBurns If you know that there would be no other types passed to `equals:`, and that the `title` can be used to identify a custom annotation uniquely, you can code the method in one line: `return [[self title] isEqualToString:[other title]];` You do not even need a cast, because square bracket notation lets you send almost any method to a variable of the *id* type. – Sergey Kalinichenko Mar 04 '14 at 18:47
  • 3
    Don't forget to also implement a proper `hash` method. This should always be done when creating an `isEqual:` method on a class. – rmaddy Mar 04 '14 at 18:47
  • Maddy: yeah, ran across that in my research, but my head started spinning. Is there an easy answer how to do what you're saying? Thanks. – james Burns Mar 04 '14 at 18:54
  • 1
    @jamesBurns If you have a unique property `title`, you can implement `hash` in a single line, too: `-(NSUInteger)hash {return [self.title hash];}` – Sergey Kalinichenko Mar 04 '14 at 18:56
  • @jamesBurns If you are looking for a guide on overriding `isEqual:`/`hash` in Cocoa, here is a [link to a question](http://stackoverflow.com/q/254281/335858) that gives you a good place to start. – Sergey Kalinichenko Mar 04 '14 at 19:01
  • dasblinkenlight - ah It works. Haven't added the hash method yet. I guess I HAVE to, or is it just good form? Also: so can you explain in which cases containsObject: works without custom isEqual: method? Confused. But grateful. – james Burns Mar 04 '14 at 19:08
  • @jamesBurns Although you do not have to override `hash` in the sense that your program would continue to work, not doing it is indeed bad form, because it may easily confuse readers of your program. You can skip overriding `isEqual:` when the base class has a good override already, or when each instance of your custom class represents a unique object (this is somewhat rare), in which case the `NSObject`'s implementation works fine for you. – Sergey Kalinichenko Mar 04 '14 at 19:21
  • @dasblinkenlight - Thanks. So the hash is just added as a method in my custom object, right? Thanks again. – james Burns Mar 04 '14 at 19:24
  • @jamesBurns Yes, `hash` needs to be an instance method on your custom object. – Sergey Kalinichenko Mar 04 '14 at 19:26
  • 1
    Great walkthrough of `isEqual:` and `hash` implementation by Mike Ash: https://www.mikeash.com/pyblog/friday-qa-2010-06-18-implementing-equality-and-hashing.html @jamesBurns – jscs Mar 04 '14 at 19:51
  • 1
    @dasblinkenlight I would state that if you implement an `isEqual:` method in your class then you *must* implement a proper `hash` method too. It is a requirement that if two objects return `YES` when compared via `isEqual:`, then `hash` *must* return the same value for both objects. The default implementation of `hash` would violate this rule unless both objects were in fact the same object instance. So no, your program will not work properly if you implement `isEqual:` but not `hash`. – rmaddy Mar 04 '14 at 23:59
0

Or you could use an NSOrderedSet (or mutable if needed) which is designed to do all your set membership functions, as well as having all your index type functions you'd expect from an NSArray.

You can transform it to array with - array when you need actual an NSArray version where an enumerable won't work.

greymouser
  • 3,133
  • 19
  • 22