It’s been a long since I wrote something. One of my loved ones is going through some health problems and I was forced to leave work aside to dedicate some time to my family. Anyway, today I would like to talk about a topic I haven’t found much information on the net: membership of custom objects in Swift arrays: finding elements in an array, checking for membership of custom objects and using them as keys for Swift dictionaries. Also, I discuss about the differences between Objective-C and Swift, and the problems that may arise if you happen to be merging them in the same project.

Swift arrays and dictionaries are a welcomed addition to the Cocoa repertoire, and I find myself using them more and more. Technically speaking, arrays and dictionaries in Swift can be easily mapped to NSArray and NSDictionary, but the devil is in the details, concretely in the membership mechanisms.

Objective-C

In Objective-C, in order to check for membership of instances of a custom class, you needed to implement two methods: isEqual: and hash. The first one would return YES if (and only if) the object passed as argument should be considered “equal” to the receiving instance, while the second one will return a hash value that will be used to build the hash table to allow the lookup routines of NSDictionary (and NSSet) to find the element in O(1).

❤️ Enjoying this post so far?

If you find this content useful, consider showing your appreciation by buying me a coffee using the button below 👇.

Buy me a coffeeBuy me a coffee

Besides that two methods, if we plan to use our custom class as a valid key in an NSDictionary, we would need to implement the NSCopying protocol, which has only one method called copyWithZone:. This is because NSDictionary’s keys are copied, so they must adhere to the protocol

Thus, in Objective-C, if you had a Person class, and you wanted to be able to find concrete instances of Person in an NSArray and NSDictionary, you could define your class as follows:

@interface Person <NSCopying>
@property (nonatomic, strong) NSString * name;
@property (nonatomic) NSInteger age;
@property (nonatomic, strong) NSString * job;
- (instancetype) initWithName: (NSString *) name andAge: (NSInteger) age;
@end

@implementation Person
- (instancetype) initWithName: (NSString *) name andAge: (NSInteger) age {
   self = [super init];
   if (self) {
      self.name = name;
      self.age = age;
   }
   return self;
}

- (BOOL) isEqual: (id) object {
   if (![object isKindOfClass:[Person class]]) { return false; }
   Person * person = (Person *) object;
   if (![self.name isEqualToString: person.name]) return NO; // check names
   if (self.age != person.age) return NO; // check ages
   return YES; // otherwise suppose they are the same person.
}

- (NSUInteger) hash { // really simple hash value
   return [self.name hash] + self.age;
}

- (id) copyWithZone: (NSZone *) zone {
   Person * person = [[Person allocWithZone:zone] init];
   person.name = [self.name copy];
   person.age = self.age;
   return person;
}
@end

In this case we are using just the name and age for checking the membership and generating the hash value, so we would assume that two instances of Person are considered equal if they have the same name and age (even though they may have different jobs). The NSCopying protocol is easy to implement. You just need to create a new instance of your custom class by calling allocWithZone: and then copy the object’s properties.

With isEqual:hash and copyWithZone: defined, we can search for Person instances in an NSArray (using containsObject) and use them as keys in an NSDictionary:

Person * john1 = [[Person alloc] initWithName: @"John Smith" andAge: 23];
john1.job = @"software engineer";
Person * john2 = [[Person alloc] initWithName: @"John Smith" andAge: 23];
john2.job = @"musician";
Person * anne = [[Person alloc] initWithName: @"Anne Anderson" andAge: 31];

BOOL johnAndJohn = [john1 isEqual:john2]; // YES (even though they have different jobs)
BOOL johnAndAnne = [john1 isEqual:anne]; // NO

NSArray * VIPPeople = @[john1, john2, anne];
if ([VIPPeople containsObject: john2]) {
   NSLog(@"%@ is a VIP", john2.name); // logs "John Smith is a VIP"
}

NSDictionary * salaries = @{john1: @75000, john2: @85000, anne: @100000};
NSLog(@"Salary of john1 is %@", salaries[john1]); // logs "Salary of john1 is 75000"

Swift

In Swift, we need to implement the Equatable protocol if we want to be able to compare two instances of a custom object and check for membership in a Swift array, and we need to implement the Hashable protocol (which implicitly needs Equatable) to be able to use instances of Person as keys in a Swift dictionary. There is no extra requirement, so we don’t need to implement an NSCopying swift’s equivalent.

The Equatable protocol requires us to define an “==” function (similar to the Objective-C’s isEqual: method), while the Hashable protocol requires the implementation of a computed property (with just a getter) called hashValue (similar to the Objective-C’s hash method). Our Person class will look like this in Swift:

class Person: NSObject, Equatable, Hashable {
   var name: String
   var age: Int
   var job: String?
   
   override var hashValue: Int {
     return name.hash ^ age
   }
  
   init(name: String, age: Int) {
      self.name = name
      self.age = age
   }
}
func == (lhs: Person, rhs: Person) -> Bool {
 return (lhs.name == rhs.name) && (lhs.age == rhs.age)
}

Notice how we must define “==” as a global function, requiring a somewhat unintuitive implementation outside of the scope of our class. With just these two elements we can check for equality and membership in Swift dictionaries and arrays:

let john = Person(name: "John Smith", age: 23)
let john2 = Person(name: "John Smith", age: 34)
let john3 = Person(name: "John Smith", age: 23)
john.job = "software engineer";
john2.job = "musician";
john3.job = "writer";

// equality
if john == john3 { println("john and John3 are the same person")
} else { println("john and John3 are not the same person") } // prints "john and john3 are the same person"

// membership
let anne = Person(name: "Anne Anderson", age: 45)
let patrick = Person(name: "Patrick McDonald", age: 61)
let mike = Person(name: "Mike Henderson", age: 15)
let VIPPeople = [anne, patrick]
if (contains(VIPPeople, john)) { println("john is a VIP Person")
} else { println("john is not a VIP") } // prints "john is not a VIP"

if let index = find(VIPPeople, anne) {
   println("Anne is VIP number \(index)") // prints "Anne is VIP number 0"
}

// use as dictionary key
let salaries = [john: 75000, john2: 85000, anne: 100000]
println("Salary of john is \(salaries[john])") // prints "Salary of john is 75000"

A word of warning

It’s extremely important to notice that “==” and hashValue are NOT mapped to their NSObject counterparts isEqual: and hash. This means that even if you define your swift class as Hashable and Equatable (and as a subclass of NSObject), and  implement the “==” function and the hashValue property, your class would still not be able to be compared using the isEqual: method, and the hash value will be completely different to your hashValue. I don’t know why Apple decided not to link them together, but it can be the cause of problems if you are merging Objective-C and Swift code in the same project. Make sure to check and re-check your Swift classes to properly work for NSArrays and NSDictionaries. As a general practice, I recommend explicitly linking them in your implementation. For example, in our Swift Person class, we could do the following:

class Person: NSObject, Equatable, Hashable { 
   ... 
   override var hashValue: Int { 
      // for swift arrays and dictionaries 
      return name.hash ^ age 
   } 

   func hash() -> Int { 
      // for NSArrays and NSDictionaries 
      return self.hashValue 
   } (see note below)

   override func isEqual(object: AnyObject?) -> Bool { 
      // for isEqual: 
      if let person = object as? Person { 
         return person == self // just use our "==" function 
      } else { return false } 
   } 
   ... 
} 
func == (lhs: Person, rhs: Person) -> Bool { 
   // for swift == comparision and membership. 
   return (lhs.name == rhs.name) && (lhs.age == rhs.age) 
}

Update: starting from XCode 6.3 and Swift 1.2, hash must be overriden as a NSObject property, so instead of declaring it as an Int function you should declare it as follows:

override var hash: Int {
   return self.hashValue
}