Search code examples
swiftoption-typekeypaths

Accessing the optional value of a PartialKeyPath in Swift 4


Using the PartialKeyPath API, how can you access a value of a key path's reference? For example, this works for non-optional values, but not with Optional values.

The issue I'm having is that self[keyPath: keyPath] returns a non-optional Any value.

struct Element {

    let name: String
    let mass: Double?

    func stringValue(_ keyPath: PartialKeyPath<Element>) -> String {
         let value = self[keyPath: keyPath]

         switch value {
         case let string as String:
             return string.capitalized
         case nil:
             return "N/A"
         case let value:
             return String(describing: value)
         }
    }
}

let element = Element(name: "Helium", mass: 4.002602)
let string = element.stringValue(\Element.mass) /* Optional(4.002602) */

The result is that case nil is never executed and the last case is being printed as Optional(value).

How can I unwrap value properly to extract the optional?


Solution

  • The solution was to use Mirror to unwrap the optional which seems less than optimal. Looking forward to better Reflection support in Swift!

    func unwrap(_ value: Any) -> Any? {
        let mirror = Mirror(reflecting: value)
    
        if mirror.displayStyle != .optional {
            return value
        }
    
        if let child = mirror.children.first {
            return child.value
        } else {
            return nil
        }
    }
    
    struct Element {
    
        let name: String
        let mass: Double?
    
        func stringValue(_ keyPath: PartialKeyPath<AtomicElement>) -> String {
            guard let value = unwrap(self[keyPath: keyPath]) else {
                return "N/A"
            }
    
            switch value {
            case let string as String:
                return string.capitalized
            default:
                return String(describing: value)
            }
        }
    }
    
    let element = Element(name: "Helium", mass: 4.002602)
    let string = element.stringValue(\Element.mass) /* 4.002602 */