Using reflection to set object properties without using setValue forKey

后端 未结 2 1339
悲&欢浪女
悲&欢浪女 2021-02-01 06:36

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

  • nullable type fields like Int?
  • properties that have an enum
相关标签:
2条回答
  • 2021-02-01 06:48

    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.

    0 讨论(0)
  • 2021-02-01 07:00

    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")
    0 讨论(0)
提交回复
热议问题