21

Let's say I have that struct:

struct MyStruct {
    let x: Bool
    let y: Bool
}

In Swift 4 we can now access it's properties with the myStruct[keyPath: \MyStruct.x] interface.

What I need is a way to access all it's key paths, something like:

extension MyStruct {

    static func getAllKeyPaths() -> [WritableKeyPath<MyStruct, Bool>] {
        return [
            \MyStruct.x,
            \MyStruct.y
        ]
    }

}

But, obviously, without me having to manually declare every property in an array.

How can I achieve that?

Rodrigo Ruiz
  • 4,248
  • 6
  • 43
  • 75

3 Answers3

7

After modifying rraphael's answer I asked about this on the Swift forums.

It is possible, discussion here:

Getting KeyPaths to members automatically using Mirror

Also, the Swift for TensorFlow team has this already built in to Swift for TensorFlow, which may make its way to pure swift:

Dynamic property iteration using key paths

Porter Child
  • 132
  • 1
  • 5
6

DISCLAIMER:

Please note that the following code is for educational purpose only and it should not be used in a real application, and might contains a lot of bugs/strange behaviors if KeyPath are used this way.

Answer:

I don't know if your question is still relevant today, but the challenge was fun :)

This is actually possible using the mirroring API.

The KeyPath API currently doesn't allow us to initialize a new KeyPath from a string, but it does support dictionary "parsing".

The idea here is to build a dictionary that will describe the struct using the mirroring API, then iterate over the key to build the KeyPath array.

Swift 4.2 playground:

protocol KeyPathListable {
  // require empty init as the implementation use the mirroring API, which require
  // to be used on an instance. So we need to be able to create a new instance of the 
  // type.
  init()

  var _keyPathReadableFormat: [String: Any] { get }
  static var allKeyPaths: [KeyPath<Foo, Any?>] { get }
}

extension KeyPathListable {
  var _keyPathReadableFormat: [String: Any] {
    let mirror = Mirror(reflecting: self)
    var description: [String: Any] = [:]
    for case let (label?, value) in mirror.children {
      description[label] = value
    }
    return description
  }

  static var allKeyPaths: [KeyPath<Self, Any?>] {
    var keyPaths: [KeyPath<Self, Any?>] = []
    let instance = Self()
    for (key, _) in instance._keyPathReadableFormat {
      keyPaths.append(\Self._keyPathReadableFormat[key])
    }
    return keyPaths
  }
}

struct Foo: KeyPathListable {
  var x: Int
  var y: Int
}

extension Foo {
  // Custom init inside an extension to keep auto generated `init(x:, y:)`
  init() {
    x = 0
    y = 0
  }
}

let xKey = Foo.allKeyPaths[0]
let yKey = Foo.allKeyPaths[1]

var foo = Foo(x: 10, y: 20)
let x = foo[keyPath: xKey]!
let y = foo[keyPath: yKey]!

print(x)
print(y)

Note that the printed output is not always in the same order (probably because of the mirroring API, but not so sure about that).

rraphael
  • 10,041
  • 2
  • 25
  • 33
  • 2
    In Swift 4.2, enumerating a dictionary is guaranteed to be a random order, different on different runs of the app. So that’s probably why you get the results in different orders. – matt Sep 20 '18 at 13:53
  • 7
    I stared at this way too long to realize you answered the literal question but not the spirit of the question. Clever, though. For those wondering: the KeyPaths are indexing into a Dictionary constructed with Mirror - they aren't read/write KeyPaths to the struct's properties. – xtravar Feb 01 '19 at 05:00
0

I propose my solution. It has the advantage of dealing correctly with @Published values when using the Combine framework.

For the sake of clarity, it is a simplified version of what I have really. In the full version, I pass some options to the Mirror.allKeyPaths() function to change behaviour ( To enumerate structs and/or classes properties in sub-dictionaries for example ).

  • The first Mirror extension propose some functions to simplify properties enumeration.
  • The second extension implements the keyPaths dictionaries creation, replacing @Published properties by correct name and value
  • The last part is the KeyPathIterable protocol, that add enumeration capability to associated object

swift

// MARK: - Convenience extensions

extension String {
    /// Returns string without first character
    var byRemovingFirstCharacter: String {
        guard count > 1 else { return "" }
        return String(suffix(count-1))
    }
}

// MARK: - Mirror convenience extension

extension Mirror {
    
    /// Iterates through all children
    static func forEachProperty(of object: Any, doClosure: (String, Any)->Void) {
        for (property, value) in Mirror(reflecting: object).children where property != nil {
            doClosure(property!, value)
        }
    }
    
    /// Executes closure if property named 'property' is found
    ///
    /// Returns true if property was found
    @discardableResult static func withProperty(_ property: String, of object: Any, doClosure: (String, Any)->Void) -> Bool {
        for (property, value) in Mirror(reflecting: object).children where property == property {
            doClosure(property!, value)
            return true
        }
        return false
    }
    
    /// Utility function to determine if a value is marked @Published
    static func isValuePublished(_ value: Any) -> Bool {
        let valueTypeAsString = String(describing: type(of: value))
        let prefix = valueTypeAsString.prefix { $0 != "<" }
        return prefix == "Published"
    }
}

// MARK: - Mirror extension to return any object properties as [Property, Value] dictionary

extension Mirror {
    
    /// Returns objects properties as a dictionary [property: value]
    static func allKeyPaths(for object: Any) -> [String: Any] {
        var out = [String: Any]()
        
        Mirror.forEachProperty(of: object) { property, value in
            // If value is of type Published<Some>, we transform to 'regular' property label and value
            if Self.isValuePublished(value) {
                Mirror.withProperty("value", of: value) { _, subValue in
                    out[property.byRemovingFirstCharacter] = subValue
                }
            } else {
                out[property] = value
            }
        }
        return out
    }
}

// MARK: - KeyPathIterable protocol

protocol KeyPathIterable {
    
}

extension KeyPathIterable {
    /// Returns all object properties
    var allKeyPaths: [String: Any] {
        return Mirror.allKeyPaths(for: self)
    }
}
Moose
  • 2,607
  • 24
  • 23