Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using reflection to set object properties without using setValue forKey

Tags:

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

  • nullable type fields like Int?
  • properties that have an enum as it's type
  • an Array of nullable objects like [MyObject?]

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.")
        }
    }
}
like image 813
Edwin Vermeer Avatar asked Jul 23 '15 13:07

Edwin Vermeer


2 Answers

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:

  • This is probably pretty fragile, you really shouldn't use this.
  • But maybe it could live in a thoroughly tested and updated reflection library until Swift gets a proper reflection API.
  • It's not that far away from what Mirror does internally, see the code in Reflection.mm, around here: https://github.com/apple/swift/blob/swift-2.2-branch/stdlib/public/runtime/Reflection.mm#L719
  • The same technique applies to the other types that KVO rejects, but you need to be careful to use the right UnsafeMutablePointer type. Particularly with protocol vars, which are 40 or 16 bytes, unlike a simple class optional which is 8 bytes (64 bit). See Mike Ash on the topic of Swift memory layout: https://mikeash.com/pyblog/friday-qa-2014-08-01-exploring-swift-memory-layout-part-ii.html

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.

like image 113
Jon N Avatar answered Oct 04 '22 12:10

Jon N


Unfortunately, this is impossible to do in Swift.

KVC is an Objective-C thing. Pure Swift optionals (combination of Int and Optional) do not work with KVC. The best thing to do with Int? would be to replace with NSNumber? and KVC will work. This is because NSNumber is still an Objective-C class. This is a sad limitation of the type system.

For your enums though, there is still hope. This will not, however, reduce the amount of coding that you would have to do, but it is much cleaner and at its best, mimics the KVC.

  1. Create a protocol called Settable

    protocol Settable {
       mutating func setValue(value:String)
    }
    
  2. Have your enum confirm to the protocol

    enum Types : Settable {
        case  FirstType, SecondType, ThirdType
        mutating func setValue(value: String) {
            if value == ".FirstType" {
                self = .FirstType
            } else if value == ".SecondType" {
                self = .SecondType
            } else if value == ".ThirdType" {
                self = .ThirdType
            } else {
                fatalError("The value \(value) is not settable to this enum")
            }
       }
    }
    
  3. Create a method: setEnumValue(value:value, forKey key:Any)

    setEnumValue(value:String forKey key:Any) {
        if key == "types" {
          self.types.setValue(value)
       } else {
          fatalError("No variable found with name \(key)")
       }
    }
    
  4. You can now call self.setEnumValue(".FirstType",forKey:"types")
like image 26
avismara Avatar answered Oct 04 '22 13:10

avismara