Using reflection to set object properties without

2020-05-18 23:41发布

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.")
        }
    }
}

2条回答
你好瞎i
2楼-- · 2020-05-19 00:23

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.

查看更多
放我归山
3楼-- · 2020-05-19 00:32

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")
查看更多
登录 后发表回答