iosswiftgenericsswift-keypath

Storing KeyPaths in Dictionary/HashMap


I'm facing a problem when hashing a ReferenceWritableKeyPath. It appears that the hash function also takes the generic properties of the ReferenceWritableKeyPath into account when hashing the key path. I've included sample code to show why this is a problem:

struct TestStruct<T> {
    // This function should only be callable if the value type of the path reference == T
    func doSomething<Root>(object: Root, path: ReferenceWritableKeyPath<Root, T>) -> Int {
        // Do something
        print("Non-optional path:   \(path)                    \(path.hashValue)")
        return path.hashValue
    }
}

let label = UILabel()
let textColorPath = \UILabel.textColor

let testStruct = TestStruct<UIColor>()
let hash1 = testStruct.doSomething(object: label, path: \.textColor)
let hash2 = textColorPath.hashValue
print("Optional path:       \(textColorPath)    \(hash2)")

If you run the code above, you will notice that hash1 and hash2 are different despite being paths to the same property of the UILabel.

This happens because the 1st ReferenceWritableKeyPath has a Value that is UIColor while the 2nd ReferenceWritableKeyPath has a Value that is Optional<UIColor>

My project requires the ReferenceWritableKeyPaths to be stored in a dictionary so that there is only one keyPath for each property of the associated object (UILabel). Since the hashes are different, this means that the same path will be stored as 2 different keys in the dictionary.

Does anyone know of a way that I can get this to work?

~Thanks in advance


Solution

  • As @Rob Napier pointed out, the problem was with the generic types themselves. The way I fixed the problem was by splitting the doSomething into two separate methods:

    func doSomething<Root>(object: Root, path: ReferenceWritableKeyPath<Root, T?>) -> Int {
        // Do something
        print("Non-optional path:   \(path)                    \(path.hashValue)")
        return path.hashValue
    }
    
    func doSomething<Root>(object: Root, path: ReferenceWritableKeyPath<Root, T>) -> Int {
        // Do something
        print("Non-optional path:   \(path)                    \(path.hashValue)")
        return path.hashValue
    }
    

    The 1st one will get called when T is an optional type such as in the example above (where UIColor can be nil). The 2nd one gets called when the keyPath points to a non-optional property. Swift is pretty smart so I guess it's able to figure out which metthod to call despite them having almost duplicate headers.