22

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.")
        }
    }
}
Edwin Vermeer
  • 13,017
  • 2
  • 34
  • 58
  • 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. – s1ddok Aug 02 '15 at 13:51
  • well, I'm able to get the values. Now I want to set the values – Edwin Vermeer Aug 02 '15 at 13:54
  • are you able to get them without mirror type? – s1ddok Aug 02 '15 at 13:54
  • You do need to get the MirrorType with reflect(..) See the valueForAny method at the bottom of: https://github.com/evermeer/EVReflection/blob/master/EVReflection/pod/EVReflection.swift – Edwin Vermeer Aug 02 '15 at 13:57
  • 1
    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. – s1ddok Aug 02 '15 at 13:59

3 Answers3

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.

Jon N
  • 1,439
  • 1
  • 18
  • 29
  • Very cool code! You are right that it's dangerous code. I still will play around with this to see its potential. – Edwin Vermeer Apr 09 '16 at 12:13
  • Works great!!! And I found something interesting (definitely should not be used in production): Seems you can create any-type mapper just using UnsafeMutablePointer (instead of UnsafeMutablePointer in your code). Totally unsafe but very cool – Anton Belousov Apr 13 '16 at 22:55
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")
avismara
  • 5,141
  • 2
  • 32
  • 56
  • 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")) { – Edwin Vermeer Aug 02 '15 at 13:29
  • 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! – avismara Aug 02 '15 at 13:32
  • 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") }) – Edwin Vermeer Aug 02 '15 at 13:34
  • Are you sure? I just whipped up my Xcode 7 Beta only to find that there is no performSelector. – avismara Aug 02 '15 at 13:38
  • 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.html – Edwin Vermeer Aug 02 '15 at 13:41
  • and it's in the release notes http://adcdownload.apple.com/Developer_Tools/Xcode_7_beta_4/Xcode_7_beta_4_Release_Notes.pdf – Edwin Vermeer Aug 02 '15 at 13:48
  • Apparently it is available for only watchOS 2 and later. – avismara Aug 02 '15 at 13:50
  • 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? – Edwin Vermeer Aug 05 '15 at 07:10
  • 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")` – avismara Aug 05 '15 at 07:17
2

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.

iUrii
  • 11,742
  • 1
  • 33
  • 48