-1

I am having an issue trying to adopt the Hashable and NSCoding protocols within the same Swift class (or struct). Hashability and encode/decode both work independently. However, hashability is lost once an object has been restored by NSCoder.

First, I am having to make this a class instead of a struct since apparently NSCoding will not work with structs. Therefore, I apparently need to inherit from NSObject (which also happens to be Hashable). Unfortunately this built-in hashability of NSObject seems to be preventing me from making my own class hashable in the manner I want (I think.)

Here is my class:

class TileMapCoords : NSObject, NSCoding {

var row: Int
var column: Int

init(row: Int, column: Int) {
    self.row = row
    self.column = column
}

// Hashable
override var hashValue: Int {
    return column*numTMRows + row
}

static func == (lhs: TileMapCoords, rhs: TileMapCoords) -> Bool {
    return
        lhs.row == rhs.row &&
            lhs.column == rhs.column
}

// do I even need this?
override func isEqual(_ object: Any?) -> Bool {

    if let otherCoords = object as? TileMapCoords {
        return self == otherCoords
    } else  {
        return false
    }
}

// NSCoding
required init?(coder aDecoder: NSCoder) {
    row = aDecoder.decodeInteger(forKey: "TileMapCoords.row")
    column = aDecoder.decodeInteger(forKey: "TileMapCoords.column")
}

func encode(with aCoder: NSCoder) {
    aCoder.encode(row, forKey: "TileMapCoords.row")
    aCoder.encode(column, forKey: "TileMapCoords.column")
}

}

I am creating a set of these objects and encoding/decoding them as follows:

Encode:

aCoder.encode(itemsCollected, forKey: "GameData.itemsCollected")

Decode:

var itemsCollected = Set<TileMapCoords>()
itemsCollected = aDecoder.decodeObject(forKey: "GameData.itemsCollected") as! Set<TileMapCoords>

But then I found the objects in the set had different hash values upon decoding than they had upon encoding. So I made a workaround which I believed worked:

let itemsCollectedCopy = aDecoder.decodeObject(forKey: "GameData.itemsCollected") as! Set<TileMapCoords>

var itemsCollected = Set<TileMapCoords>()
for coords in itemsCollectedCopy {
        itemsCollected.insert(coords) }

This "trick" seemed to correct the hash values when I printed them out, however the objects are still not being properly hashed (for reasons unknown). Namely when I do this:

if curGameData!.itemsCollected.contains(location) {
     // do something      
 }

If I create a TileMapCoords object with the same row and column as an object in the itemsCollected set, the 'contains' method tells me it

1) Is in the set initially 2) Is not in the set once the set has been restored/decoded by NSCoder

The strange that is that the object IS in the set and I have verified that both my object and the one in the set have the same hash value, and are equal according to the == operator and the isEqual method.

I need help solving this issue or thinking of another means to have both hashability and NSCodability within the same class (perhaps not overriding NSObject?)

ksgg
  • 31
  • 3
  • Can you explain why you need to implement NSCoding? The whole point of Codable is that it makes it easy to encode a struct, and there are no situations where you would use NSCoding where you can't use Codable instead. So why not just use Codable? – matt Jul 22 '18 at 22:53
  • Your code works just fine for me without any need to build a second set after decoding. Please update your question with a fully reproducible example demonstrating your issue. – rmaddy Jul 22 '18 at 22:56
  • The code works fine until I encode the set and then decode it. Then the 'contains' method no longer seems to find an (identical) object in the set. – ksgg Jul 22 '18 at 23:00
  • Ok, honestly I was not aware of Codable. If I can use that with a struct then that would be perfect. I will try to implement it. – ksgg Jul 22 '18 at 23:01
  • As I said, your code works for me. Even after encoding and decoding. That's why you need to post a fully working example that demonstrates the issue. Post something that can be copied and pasted as-is into a playground. – rmaddy Jul 22 '18 at 23:13
  • Ok I am currently working on getting the code extracted into a simple example. It is taking some time to do. – ksgg Jul 24 '18 at 02:04
  • I just figured it out. The code I posted does indeed work. I had made an error elsewhere which was altering the value of what should have been a constant in the hash function. – ksgg Jul 24 '18 at 04:11

1 Answers1

0

To customize hashing and equality in NSObject subclasses, you need to override the hash property and the isEqual(_:) method.

While the Swift-only NSObject.hashValue property is unfortunately overridable (as of Swift 4.1), you must never override it. Override hash instead, so that Foundation picks up the correct hashing behavior -- otherwise Foundation will use the implementation inherited from NSObject, which is based on object identity. This is inappropriate for TileMapCoords, which has a custom isEqual(_:) implementation.

Sets decoded via NSCoding are first created as NSSet instances, which are then bridged to Swift's Set type. NSSet is a Foundation class, so it uses Foundation's hashing API -- which is unfortunately broken for TileMapCoords above, leading to the strange behavior you noticed. Replacing hashValue with hash will fix the issue.

Karoy Lorentey
  • 4,843
  • 2
  • 28
  • 28