Chris Prince
Chris Prince

Reputation: 7584

Getting the value of a property using it's string name in pure Swift using reflection

I want to use Swift (not Objective-C runtime) Reflection to create a method like this:

func valueFor(property:String, of object:Any) -> Any? {
    ...
}

To some extent, I can do this using:

func valueFor(property:String, of object:Any) -> Any? {
    let mirror = Mirror(reflecting: object)
    return mirror.descendant(property)
}

With

class TestMe {
    var x:Int!
}

let t = TestMe()
t.x = 100
let result = valueFor(property: "x", of: t)
print("\(result); \(result!)")

This prints out what I'd expect:

Optional(100); 100

When I do:

let t2 = TestMe()    
let result2 = valueFor(property: "x", of: t2)
print("\(result2)")

The output is:

Optional(nil)

This might seem reasonable, except that if I do:

var x:Int!
print("\(x)")

This prints out:

nil

and not Optional(nil). The bottom line is that I'm having difficulty programmatically determining that the value of t2.x is nil using my valueFor method.

If I continue the above code with:

if result2 == Optional(nil)! {
    print("Was nil1")
}

if result2 == nil {
    print("Was nil2")
}

Neither of these print statements output anything.

When I put a breakpoint into Xcode and look at the value of result2 with the debugger, it shows:

▿ Optional<Any>
  - some : nil

So, my question is: How can I determine if the original member variable was nil using the result from valueFor?

Additional1: If I do:

switch result2 {
case .some(let x):
    // HERE
    break
default:
    break
}

and put a breakpoint at HERE, the value of x turns out to be nil. But, even if I assign it to an Any?, comparing it to nil is not true.

Additional2: If I do:

switch result2 {
case .some(let x):
    let z:Any? = x
    print("\(z)")
    if z == nil {
        print("Was nil3")
    }
    break
default:
    break
}

This prints out (only):

Optional(nil)

I find this especially odd. result2 prints out exactly the same thing!

Upvotes: 8

Views: 3129

Answers (2)

Johnathon Karcz
Johnathon Karcz

Reputation: 76

well, i know it has been 4 years, but I am on Xcode 12 and still facing the same issue. since this question seems to be unanswered, I will add what worked for me.

func valueFor(property: String, of object: Any) -> Any? {
    let optionalPropertyName = "some"
    let mirror = Mirror(reflecting: object)
    if let child = mirror.descendant(property) {
        if let optionalMirror = Mirror(reflecting: child), optionalMirror.displayStyle == DisplayStyle.optional {
            return optionalMirror.descendant(optionalPropertyName)
        } else {
            return child
        }
    } else {
        return nil
    }
}

by using Mirror to check for optional and then extract the optional using "some" you get back either a true object or nil. when this is returned to the caller via the Any? return, you are now able to nil check the value and have that work appropriately.

Upvotes: 0

Chris Prince
Chris Prince

Reputation: 7584

This is a bit of a hack, but I think it's going to solve the problem for me. I'm still looking for better solutions:

func isNilDescendant(_ any: Any?) -> Bool {
    return String(describing: any) == "Optional(nil)"
}

func valueFor(property:String, of object:Any) -> Any? {
    let mirror = Mirror(reflecting: object)
    if let child = mirror.descendant(property), !isNilDescendant(child) {
        return child
    }
    else {
        return nil
    }
}

Upvotes: 2

Related Questions