Successfully using KVC with Swift Structs

135 Views Asked by At

I'm facing a very niche problem but hope someone can help me. I'm using KVC in my project to set values in a class. This class contains properties including some that are structs. I was able to make these structs @objc compatible using the _ObjectiveCBridgeable protocol (I know it's not ideal but that's the best method I found for this). Now when I try to use setValue(_:forKeyPath:) on the parent class to set a value for one of the properties in the struct, it doesn't work. After some debugging I understand that it's bridging the struct to its Objective-C equivalent class and setting the value on that Objective-C equivalent class, but it's not converting the Objective-C class back to the struct and updating the struct on the parent class.

I think it should be possible as can be seen in https://stackoverflow.com/questions/23216673/how-can-i-animate-the-height-of-a-calayer-with-the-origin-coming-from-the-bottom#:~:text=Then%20for%20the%20animation%2C%20you%20simply%20animate%20the%20bounds%20(or%20even%20better%2C%20you%20can%20animate%20%22bounds.size.height%22%2C%20since%20only%20the%20height%20is%20changing)%3A where it is stated that you can animate you can animate bounds.size.height using its keyPath in a CABasicAnimation

Here's my code (which can run in a Swift playground):

@available(iOS 2.0, *)
@objc public class TestClass: NSObject {
    @objc public var x: Int
    @objc public var y: Int
    @objc public var testStruct = TestStruct(z: 0)
    
    init(x:Int, y: Int) {
        self.x = x
        self.y = y
    }
}

@available(iOS 2.0, *)
public struct TestStruct: _ObjectiveCBridgeable {
    public typealias _ObjectiveCType = NSTestStruct
    
    public var z: Int
    
    public init(z: Int) { self.z = z }
    
    public func _bridgeToObjectiveC() -> NSTestStruct {
        return NSTestStruct(z: z)
    }
    public static func _forceBridgeFromObjectiveC(_ source: NSTestStruct, result: inout TestStruct?) {
        result = TestStruct(z: source.z)
    }
    public static func _unconditionallyBridgeFromObjectiveC(_ source: NSTestStruct?) -> TestStruct {
        return TestStruct(z: source?.z ?? 0)
    }
    public static func _conditionallyBridgeFromObjectiveC(_ source: NSTestStruct, result: inout TestStruct?) -> Bool {
        result = TestStruct(z: source.z)
        return true
    }
}

@available(iOS 2.0, *)
@objc public class NSTestStruct: NSObject {
    @objc public var z: Int // Updates to "12"
    @objc public init(z: Int) {
        self.z = z
    }
}

var testClass = TestClass(x: 0, y: 0)
testClass.setValue(12, forKeyPath: "testStruct.z")
print(testClass.testStruct.z) // Prints "0", expected "12"
1

There are 1 best solutions below

0
On BEST ANSWER

As Martin R mentioned, CABasicAnimation has a custom setValue(_:forKeyPath:) which allows you to set nested properties. I coded a custom setValue(_:forKeyPath:) which does the same thing for a general keyPath containing structs who's types are directly convertible to Objective-C (same property names and types). Here's my solution:

public extension NSObject {
    func setValueRecursively(_ value: Any?, forKeyPath keyPath: String) {
        var currentKeyPath = keyPath
        var currentValue: Any? = value
        while true {
            guard let lastPeriodIndex = currentKeyPath.lastIndex(of: ".") else {
                self.setValue(currentValue, forKey: currentKeyPath)
                return
            }
            
            let parentKeyPath = String(currentKeyPath[currentKeyPath.startIndex ..< lastPeriodIndex])
            guard let parentValue = self.value(forKeyPath: parentKeyPath) as? NSObject else { return }
            parentValue.setValue(currentValue, forKey: String(currentKeyPath.suffix(currentKeyPath.count-parentKeyPath.count-1)))
            currentValue = parentValue
            currentKeyPath = parentKeyPath
        }
    }
}

With the same code as the Question,

testClass.setValueRecursively(12, forKeyPath: "testStruct.z")

works as expected :)

Note: This will not work with CGSize, CGRect, and all other properties that are represented as NSValue