'Using reflection to set object properties without using setValue forKey

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


Solution 1:[1]

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.

Solution 2:[2]

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

Solution 3:[3]

Swift 5

To set and get properties values with pure swift types you can use internal ReflectionMirror.swift approach with shared functions:

  • swift_reflectionMirror_recursiveCount
  • swift_reflectionMirror_recursiveChildMetadata
  • swift_reflectionMirror_recursiveChildOffset

The idea is to gain info about an each property of an object and then set a value to a needed one by its pointer offset.

There is example code with KeyValueCoding protocol for Swift that implements setValue(_ value: Any?, forKey key: String) method:

typealias NameFreeFunc = @convention(c) (UnsafePointer<CChar>?) -> Void

struct FieldReflectionMetadata {
    let name: UnsafePointer<CChar>? = nil
    let freeFunc: NameFreeFunc? = nil
    let isStrong: Bool = false
    let isVar: Bool = false
}

@_silgen_name("swift_reflectionMirror_recursiveCount")
fileprivate func swift_reflectionMirror_recursiveCount(_: Any.Type) -> Int

@_silgen_name("swift_reflectionMirror_recursiveChildMetadata")
fileprivate func swift_reflectionMirror_recursiveChildMetadata(
    _: Any.Type
    , index: Int
    , fieldMetadata: UnsafeMutablePointer<FieldReflectionMetadata>
) -> Any.Type

@_silgen_name("swift_reflectionMirror_recursiveChildOffset")
fileprivate func swift_reflectionMirror_recursiveChildOffset(_: Any.Type, index: Int) -> Int

protocol Accessors {}
extension Accessors {
    static func set(value: Any?, pointer: UnsafeMutableRawPointer) {
        if let value = value as? Self {
            pointer.assumingMemoryBound(to: self).pointee = value
        }
    }
}

struct ProtocolTypeContainer {
    let type: Any.Type
    let witnessTable = 0
    
    var accessors: Accessors.Type {
        unsafeBitCast(self, to: Accessors.Type.self)
    }
}

protocol KeyValueCoding {
}

extension KeyValueCoding {
    
    private mutating func withPointer<Result>(displayStyle: Mirror.DisplayStyle, _ body: (UnsafeMutableRawPointer) throws -> Result) throws -> Result {
        switch displayStyle {
        case .struct:
            return try withUnsafePointer(to: &self) {
                let pointer = UnsafeMutableRawPointer(mutating: $0)
                return try body(pointer)
            }
        case .class:
            return try withUnsafePointer(to: &self) {
                try $0.withMemoryRebound(to: UnsafeMutableRawPointer.self, capacity: 1) {
                    try body($0.pointee)
                }
            }
        default:
            fatalError("Unsupported type")
        }
    }
    
    public mutating func setValue(_ value: Any?, forKey key: String) {
        let mirror = Mirror(reflecting: self)
        guard let displayStyle = mirror.displayStyle
                , displayStyle == .class || displayStyle == .struct
        else {
            return
        }
        
        let type = type(of: self)
        let count = swift_reflectionMirror_recursiveCount(type)
        for i in 0..<count {
            var field = FieldReflectionMetadata()
            let childType = swift_reflectionMirror_recursiveChildMetadata(type, index: i, fieldMetadata: &field)
            defer { field.freeFunc?(field.name) }
            guard let name = field.name.flatMap({ String(validatingUTF8: $0) }),
                  name == key
            else {
                continue
            }
            
            let clildOffset = swift_reflectionMirror_recursiveChildOffset(type, index: i)
            
            try? withPointer(displayStyle: displayStyle) { pointer in
                let valuePointer = pointer.advanced(by: clildOffset)
                let container = ProtocolTypeContainer(type: childType)
                container.accessors.set(value: value, pointer: valuePointer)
            }
            break
        }
    }
}

This approach works with both class and struct and supports optional, enum and inherited(for classes) properties:

// Class

enum UserType {
    case admin
    case guest
    case none
}

class User: KeyValueCoding {
    let id = 0
    let name = "John"
    let birthday: Date? = nil
    let type: UserType = .none
}

var user = User()
user.setValue(12345, forKey: "id")
user.setValue("Bob", forKey: "name")
user.setValue(Date(), forKey: "birthday")
user.setValue(UserType.admin, forKey: "type")

print(user.id, user.name, user.birthday!, user.type) 
// Outputs: 12345 Bob 2022-04-22 10:41:10 +0000 admin

// Struct

struct Book: KeyValueCoding {
    let id = 0
    let title = "Swift"
    let info: String? = nil
}

var book = Book()
book.setValue(56789, forKey: "id")
book.setValue("ObjC", forKey: "title")
book.setValue("Development", forKey: "info")

print(book.id, book.title, book.info!) 
// Outputs: 56789 ObjC Development

if you are afraid to use @_silgen_name for shared functions you can access to it dynamically with dlsym e.g.: dlsym(RTLD_DEFAULT, "swift_reflectionMirror_recursiveCount") etc.

UPDATE

There is a swift package (https://github.com/ikhvorost/KeyValueCoding) with full implementation of KeyValueCoding protocol for pure Swift and it supports: get/set values to any property by a key, subscript, get a metadata type, list of properties and more.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3