Using reflection to set object properties without using setValue forKey
Asked Answered
D

3

23

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.")
        }
    }
}
Deese answered 23/7, 2015 at 13:51 Comment(5)
What I can advice you is to wait until Apple releases Swift's source in autumn, because they know how to iterate through Swift properties. (reflect function which returns MirrorType not just with a object's copy but with reference to every property), so If MirrorType would make it to open sources code parts you then can just see how they achieve that and do port that approach to your library.Evaleen
well, I'm able to get the values. Now I want to set the valuesDeese
are you able to get them without mirror type?Evaleen
You do need to get the MirrorType with reflect(..) See the valueForAny method at the bottom of: github.com/evermeer/EVReflection/blob/master/EVReflection/pod/…Deese
that is what I'm talking about. You only can get the value by using reflect function and MirrorType, but you do not know how Apple do that on backstage. They can iterate through properties somehow at runtime and we won't know how until they release the source code.Evaleen
D
15

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.

Diapause answered 9/4, 2016 at 11:50 Comment(2)
Very cool code! You are right that it's dangerous code. I still will play around with this to see its potential.Deese
Works great!!! And I found something interesting (definitely should not be used in production): Seems you can create any-type mapper just using UnsafeMutablePointer<objc_property_t> (instead of UnsafeMutablePointer<Int?> in your code). Totally unsafe but very coolPerot
D
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.

Dian answered 22/4, 2022 at 10:52 Comment(0)
B
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")
Brougham answered 2/8, 2015 at 13:23 Comment(9)
I was hoping for a workaround like setting it by using performSelector. I'm not sure if properties have get and set method calls like they have in Objective C. I tried a lot of signatures with no success. I have tested a lot using if myObject.respondsToSelector(NSSelectorFromString("setMyProperty")) {Deese
Getters and setters in Swift are very much different. There is no "selector" as such for the getters. What's more, there is no performSelector either!Brougham
In swift 2 performSelector is back and for now you could use something like: NSTimer.scheduledTimerWithTimeInterval(0.001, target: myObject, selector: Selector(sel), userInfo: [3], repeats: false) or NSThread.detachNewThreadSelector(Selector("myMethod:"), toTarget:myObject, withObject: "myValue") })Deese
Are you sure? I just whipped up my Xcode 7 Beta only to find that there is no performSelector.Brougham
yes I'm sure, here is the doc: xcdoc://?url=developer.apple.com/library/etc/redirect/xcode/ios/1048/documentation/Cocoa/Reference/Foundation/Protocols/NSObject_Protocol/index.htmlDeese
and it's in the release notes adcdownload.apple.com/Developer_Tools/Xcode_7_beta_4/…Deese
Apparently it is available for only watchOS 2 and later.Brougham
I am trying to see the benefit of your enum code. Aren't you just creating an alternative to the rawValue for an enum of type String?. Besides that the 'set value forUndefinedKey' will do almost the same as your setEnumValue method. My intention was to eliminate the need to change the original object entirely. Your solution seems like adding more code. Or am I missing something?Deese
Yes. Flexibility and readability. Suppose you have a hundred other enums. It is way easier to set it this way. Plus, self.setEnumValue(".FirstType",forKey:"types") reads way better than, self.setValue(0, forUndefinedKeyKey:"types")`Brougham

© 2022 - 2024 — McMap. All rights reserved.