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