read

Trying to achieve this might sound silly, as the goal of keypath is to eliminate string and be strict. But I do need it in a particular problem, where I need to keep a list of properties that has special behaviours.

struct Book {
    var title: String
    var rating: Int
}

// The goal is to get & set using string for the properties
var book = Book(title: "", rating: 0)
book.set("title", "Lord of the Bing")
book.get("title")

Return keypath from a string

To return PartialKeyPath given a string, we need to exhaustively list for each of the property. While this is tedious, it is possible to make use of Swift Macros to generate such repetitive code.

extension Book {
    static func keyPath(from s: String) -> PartialKeyPath<Self>? {
        switch s {
        case "title": return \Self.title
        case "rating": return \Self.rating
        default: return nil
        }
    }
}

Get

func `get`(_ s: String) -> Any? {
    guard let kp = Self.keyPath(from: s) else { return nil }
    return self[keyPath: kp]
}

Set

Setting requires duplicating the code for all the property types, as it is necessary when casting to WritableKeyPath.

mutating func `set`(_ s: String, _ x: Any) {
    guard let kp = Self.keyPath(from: s) else { return }
    switch kp {
    case let w as WritableKeyPath<Self, String>: if let x = x as? String { self[keyPath: w] = x }
    case let w as WritableKeyPath<Self, Int>: if let x = x as? Int { self[keyPath: w] = x }
    default: break
    }
}

Alternative: Use reflection

I found an implementation for KeyPathListable, which uses Mirror to provide allKeyPaths – a dictionary of property string to keypaths.

protocol KeyPathListable {
    var allKeyPaths: [String: PartialKeyPath<Self>] { get }
}

extension KeyPathListable {
    private subscript(checkedMirrorDescendant key: String) -> Any {
        return Mirror(reflecting: self).descendant(key)!
    }

    var allKeyPaths: [String: PartialKeyPath<Self>] {
        var membersTokeyPaths = [String: PartialKeyPath<Self>]()
        let mirror = Mirror(reflecting: self)
        for case (let key?, _) in mirror.children {
            membersTokeyPaths[key] = \Self.[checkedMirrorDescendant: key] as PartialKeyPath
        }
        return membersTokeyPaths
    }
}

extension Book: KeyPathListable {}

It is a handy protocol extension.

However, you can’t set using mirror..


Image

@samwize

¯\_(ツ)_/¯

Back to Home