swiftreflectionsetvalue

Using reflection to set object properties without using setValue forKey


In Swift it's not possible use .setValue(..., forKey: ...)

There is one workaround for this and that is by overriding the setValue forUndefinedKey method in the object itself.

Since I'm writing a general object mapper based on reflection. See EVReflection I would like to minimize this kind of manual mapping as much as possible.

Is there an other way to set those properties automatically?

The workaround can be found in a unit test in my library here This is the code:

class WorkaroundsTests: XCTestCase {
    func testWorkarounds() {
        let json:String = "{\"nullableType\": 1,\"status\": 0, \"list\": [ {\"nullableType\": 2}, {\"nullableType\": 3}] }"
        let status = Testobject(json: json)
        XCTAssertTrue(status.nullableType == 1, "the nullableType should be 1")
        XCTAssertTrue(status.status == .NotOK, "the status should be NotOK")
        XCTAssertTrue(status.list.count == 2, "the list should have 2 items")
        if status.list.count == 2 {
            XCTAssertTrue(status.list[0]?.nullableType == 2, "the first item in the list should have nullableType 2")
            XCTAssertTrue(status.list[1]?.nullableType == 3, "the second item in the list should have nullableType 3")
        }
    }
}

class Testobject: EVObject {
    enum StatusType: Int {
        case NotOK = 0
        case OK
    }

    var nullableType: Int?
    var status: StatusType = .OK
    var list: [Testobject?] = []

    override func setValue(value: AnyObject!, forUndefinedKey key: String) {
        switch key {
        case "nullableType":
            nullableType = value as? Int
        case "status":
            if let rawValue = value as? Int {
                status = StatusType(rawValue: rawValue)!
            }
        case "list":
            if let list = value as? NSArray {
                self.list = []
                for item in list {
                    self.list.append(item as? Testobject)
                }
            }
        default:
            NSLog("---> setValue for key '\(key)' should be handled.")
        }
    }
}

Solution

  • I found a way around this when I was looking to solve a similar problem - that KVO can't set the value of a pure Swift protocol field. The protocol has to be marked @objc, which caused too much pain in my code base. The workaround is to look up the Ivar using the objective C runtime, get the field offset, and set the value using a pointer. This code works in a playground in Swift 2.2:

    import Foundation
    
    class MyClass
    {
        var myInt: Int?
    }
    
    let instance = MyClass()
    
    // Look up the ivar, and it's offset
    let ivar: Ivar = class_getInstanceVariable(instance.dynamicType, "myInt")
    let fieldOffset = ivar_getOffset(ivar)
    
    // Pointer arithmetic to get a pointer to the field
    let pointerToInstance = unsafeAddressOf(instance)
    let pointerToField = UnsafeMutablePointer<Int?>(pointerToInstance + fieldOffset)
    
    // Set the value using the pointer
    pointerToField.memory = 42
    
    assert(instance.myInt == 42)
    

    Notes:

    Edit: There is now a framework called Runtime at https://github.com/wickwirew/Runtime which provides a pure Swift model of the Swift 4+ memory layout, allowing it to safely calculate the equivalent of ivar_getOffset without invoking the Obj C runtime. This allows setting properties like this:

    let info = try typeInfo(of: User.self)
    let property = try info.property(named: "username")
    try property.set(value: "newUsername", on: &user)
    

    This is probably a good way forward until the equivalent capability becomes part of Swift itself.