Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In Swift 4, how can you assign to a keypath when the type of the keypath and value are generic but the same?

Given:

struct Foo {
    var bar: String = “”
    var baz: Int? = nil
}
let values: [Any] = [“foo”, 1337]
let fooPaths: [PartialKeyPath<Foo>] = [\Foo.bar, \Foo.baz] 
let protoFoo = Foo()

How can I make a for loop that iterates through these keypaths and values, and assigns the values to the keypaths when the type matches for both and the keypath is writable?

Note: it must be done using generics, since Foo could have properties of any type whatsoever (except type Foo, of course).

I have tried stuff like:

func setKeyOnRoot<Root, Value>(_ root: Root, value: Value, key: PartialKeyPath<Root>) -> Root {
    var root = root
    if let writableKey = key as? WritableKeyPath<Root, Value> {
        root[keyPath: writableKey] = value
    }
    return root
}

But since value is necessarily cast as Any before calling this, the runtime fails to see the type Value as what it is.

like image 426
CommaToast Avatar asked Dec 12 '17 03:12

CommaToast


1 Answers

This implementation is similar to what you provided as an example of an approach you tried, and I believe it produces the result you are looking for:

struct WritableKeyPathApplicator<Type> {
    private let applicator: (Type, Any) -> Type
    init<ValueType>(_ keyPath: WritableKeyPath<Type, ValueType>) {
        applicator = {
            var instance = $0
            if let value = $1 as? ValueType {
                instance[keyPath: keyPath] = value
            }
            return instance
        }
    }
    func apply(value: Any, to: Type) -> Type {
        return applicator(to, value)
    }
}

struct Foo {
    var bar: String = ""
    var baz: Int? = nil
}

let values: [Any] = ["foo", 1337]
let fooPaths: [WritableKeyPathApplicator<Foo>] = [WritableKeyPathApplicator(\Foo.bar), WritableKeyPathApplicator(\Foo.baz)]
let protoFoo = zip(fooPaths, values).reduce(Foo()){ return $1.0.apply(value: $1.1, to: $0) }

print(protoFoo)  // prints Foo(bar: "foo", baz: Optional(1337))

A version of this that does the same thing, but using a for loop as specified in the original question could replace the second-to-last line with:

var protoFoo = Foo()
for (applicator, value) in zip(fooPaths, values) {
    protoFoo = applicator.apply(value: value, to: protoFoo)
}

Note that some type erasure is needed to work with a single array of keyPaths that operate on the same root type but have different value types. However, at runtime, the closure inside WritableKeyPathApplicator can specifically cast to the correct original value type for each keyPath and either silently skip the value to set if it's the wrong type (as in the example code above), or it could throw an error or print more helpful information to the console, etc.

like image 150
Daniel Hall Avatar answered Oct 12 '22 08:10

Daniel Hall