2

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

CentrumGuy
  • 576
  • 1
  • 4
  • 16

2 Answers2

1

Make textColorPath also be non-optional, to match:

let textColorPath = \UILabel.textColor!

or be explicit about the type:

let textColorPath: ReferenceWritableKeyPath<UILabel, UIColor> = \.textColor

The underlying problem is that \.textColor is an implicitly unwrapped optional rather than a "real" optional. In some contexts that gets treated as the underlying type, and in others it's promoted to an Optional. The reason it's an implicitly unwrapped optional is because it is legal to set textColor to nil. But the value you read will never be nil.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Interesting. Do you know of a way to make Swift automatically prioritize the `ReferenceWritableKeyPath` value type in `doSomething` over the `T` struct type so that the hashValue will have an optional type? (this way I don't have to force unwrap or add any extra code outside of the `TestStruct`. (This is for a library so I want to minimize the extra code outside of `TestStruct`) – CentrumGuy Feb 01 '21 at 19:47
  • Definitely: use `TestStruct` rather than `TestStruct`. You're explicitly asking for the non-optional form. But beyond that, it depends on how flexible you are with TestStruct, and whether you need to do anything other than hash. For example, this will work much more cleanly using a non-writable KeyPath. So there are lots of answers, but I expect every one of them I write up will result in "but my actual problem can't do that." So I need to understand your real restrictions and what you want real calling code to look like (and what doSomething does other than return a hash). – Rob Napier Feb 01 '21 at 20:06
  • If you always want TestStruct to take optional values, then you can require that the type be optional, but then you can't *also* make it writable, because then the property has to accept nil. – Rob Napier Feb 01 '21 at 20:08
0

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.

CentrumGuy
  • 576
  • 1
  • 4
  • 16