Swift结构体结合KVC使用问题:无法通过键路径更新属性
KVC修改Swift结构体属性不生效的问题解决
问题描述
在NSObject子类中定义了结构体类型的@objc属性,通过_ObjectiveCBridgeable协议让结构体兼容OC,但调用setValue(_:forKeyPath:)修改结构体内部属性时,操作不生效。调试发现KVC会把结构体桥接成对应的OC类实例并修改该实例的值,但不会将修改后的OC实例转换回结构体,导致原属性没有更新。
比如示例代码中,调用testClass.setValue(12, forKeyPath: "testStruct.z")后,testClass.testStruct.z仍然是0,而非预期的12。
原因分析
KVC是Objective-C的机制,当访问Swift结构体的内部属性时:
- 系统会先通过
_ObjectiveCBridgeable把结构体值拷贝成对应的OC对象; - 修改OC对象的属性后,KVC没有自动触发反向桥接(把OC对象转换回结构体)并赋值给原属性;
- 结构体是值类型,修改桥接后的OC对象副本,完全不影响原结构体实例。
至于CALayer的bounds.size.height能生效,是因为CoreAnimation框架对这类键路径做了特殊适配,属于框架层面的定制逻辑,并非普通KVC的默认行为。
解决方案
手动拦截KVC的键路径赋值,直接操作原结构体属性:
重写NSObject子类的setValue(_:forKeyPath:)方法,针对结构体内部的键路径做特殊处理,手动更新结构体实例:
@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 } override func setValue(_ value: Any?, forKeyPath keyPath: String) { // 拦截对testStruct.z的赋值请求 if keyPath == "testStruct.z" { guard let intValue = value as? Int else { super.setValue(value, forKeyPath: keyPath) return } // 创建结构体可变副本,修改后重新赋值 var updatedStruct = testStruct updatedStruct.z = intValue testStruct = updatedStruct return } // 其他键路径交给父类处理 super.setValue(value, forKeyPath: keyPath) } } // 以下是原代码中的TestStruct和NSTestStruct,无需修改 @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 @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) // 现在输出"12"
补充说明
如果结构体内部有多个属性需要通过KVC修改,可以扩展键路径的判断逻辑(比如用hasPrefix("testStruct.")来批量处理)。另外,_ObjectiveCBridgeable属于Swift内部协议,未来可能有变更风险,若长期维护建议优先考虑将结构体替换为Objective-C类(如果业务允许)。
内容的提问来源于stack exchange,提问作者CentrumGuy
相关产品推荐
相关产品推荐

