Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get all key paths from a struct in Swift 4

Tags:

swift

swift4

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?

like image 291
Rodrigo Ruiz Avatar asked Oct 01 '17 02:10

Rodrigo Ruiz


2 Answers

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).

like image 163
rraphael Avatar answered Nov 10 '22 03:11

rraphael


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

like image 6
Porter Child Avatar answered Nov 10 '22 05:11

Porter Child