如何在Category中实现基于集合的KVO兼容只读依赖属性?
为FinalClass通过Category实现KVO兼容的过滤集合属性
需求概述
无法修改或子类化FinalClass,仅能通过Category扩展一个只读属性someCollectionSubset——该属性是原类中someCollection集合的过滤子集,要求具备KVO兼容性以支持绑定。
现有方案的局限
对于非集合属性依赖场景,可通过实现keyPathsForValuesAffectingValueFor<Property>来关联依赖属性,但苹果文档明确指出,该方法不支持包含多对多关系的键路径,因此无法直接用于集合依赖场景。
苹果官方给出的两种解决方案在Category中难以落地:
- 无法安全地在原类的
init/dealloc中注册/注销观察者 - 无法通过子类扩展生命周期方法
最优实现:方法混写(Method Swizzling)
通过Objective-C运行时的方法混写技术,拦截原类中所有修改someCollection的操作,在合适时机为someCollectionSubset发送KVO变更通知,覆盖集合替换和集合内容修改两种场景。
完整代码实现
FinalClass+Category.m
#import "FinalClass+Category.h" #import <objc/runtime.h> @implementation FinalClass (Category) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // Hook 集合替换方法 SEL originalSetSel = @selector(setSomeCollection:); SEL swizzledSetSel = @selector(swizzled_setSomeCollection:); Method originalSetMethod = class_getInstanceMethod(self, originalSetSel); Method swizzledSetMethod = class_getInstanceMethod(self, swizzledSetSel); if (!class_addMethod(self, originalSetSel, method_getImplementation(swizzledSetMethod), method_getTypeEncoding(swizzledSetMethod))) { method_exchangeImplementations(originalSetMethod, swizzledSetMethod); } // Hook 可变集合获取方法 SEL originalMutableSetSel = @selector(mutableSetValueForKey:); SEL swizzledMutableSetSel = @selector(swizzled_mutableSetValueForKey:); Method originalMutableMethod = class_getInstanceMethod(self, originalMutableSetSel); Method swizzledMutableMethod = class_getInstanceMethod(self, swizzledMutableSetSel); if (!class_addMethod(self, originalMutableSetSel, method_getImplementation(swizzledMutableMethod), method_getTypeEncoding(swizzledMutableMethod))) { method_exchangeImplementations(originalMutableMethod, swizzledMutableMethod); } }); } - (void)swizzled_setSomeCollection:(NSSet *)newCollection { // 发送子集属性的KVO变更通知 [self willChangeValueForKey:@"someCollectionSubset"]; [self swizzled_setSomeCollection:newCollection]; // 调用原方法 [self didChangeValueForKey:@"someCollectionSubset"]; } - (NSMutableSet *)swizzled_mutableSetValueForKey:(NSString *)key { NSMutableSet *mutableSet = [self swizzled_mutableSetValueForKey:key]; if ([key isEqualToString:@"someCollection"]) { // 返回代理集合拦截修改操作 return [[MutableSetProxy alloc] initWithTarget:mutableSet owner:self key:@"someCollectionSubset"]; } return mutableSet; } - (NSSet *)someCollectionSubset { // 实现集合过滤逻辑 NSPredicate *predicate = [NSPredicate predicateWithFormat:@"..."]; // 替换为你的过滤条件 return [self.someCollection filteredSetUsingPredicate:predicate]; } @end #pragma mark - 可变集合代理类 @interface MutableSetProxy : NSProxy - (instancetype)initWithTarget:(NSMutableSet *)target owner:(id)owner key:(NSString *)key; @end @implementation MutableSetProxy { NSMutableSet *_targetSet; id _ownerObject; NSString *_observedKey; } - (instancetype)initWithTarget:(NSMutableSet *)target owner:(id)owner key:(NSString *)key { _targetSet = target; _ownerObject = owner; _observedKey = key; return self; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { return [_targetSet methodSignatureForSelector:sel]; } - (void)forwardInvocation:(NSInvocation *)invocation { NSString *selName = NSStringFromSelector(invocation.selector); // 判断是否为集合修改操作 BOOL isMutation = [selName hasPrefix:@"add"] || [selName hasPrefix:@"remove"] || [selName isEqualToString:@"setSet:"] || [selName isEqualToString:@"intersectSet:"] || [selName isEqualToString:@"minusSet:"] || [selName isEqualToString:@"unionSet:"]; if (isMutation) { [_ownerObject willChangeValueForKey:_observedKey]; [invocation invokeWithTarget:_targetSet]; [_ownerObject didChangeValueForKey:_observedKey]; } else { [invocation invokeWithTarget:_targetSet]; } } @end
方案说明
集合替换场景处理:
混写setSomeCollection:方法,在调用原方法前后,为someCollectionSubset发送KVO的willChange和didChange通知,覆盖直接替换整个集合的操作。集合内容修改场景处理:
混写mutableSetValueForKey:方法,当请求someCollection的可变集合时,返回一个NSProxy代理对象。代理拦截所有修改集合的操作(增删、集合运算等),在执行前后发送子集属性的KVO通知。线程安全保障:
在load方法中使用dispatch_once确保方法混写仅执行一次,避免重复混写引发的逻辑异常。
注意事项
- 若原类存在自定义的集合修改方法(非通过
mutableSetValueForKey:的操作),需额外混写这些方法以保证KVO通知的完整性。 - NSProxy的方法拦截逻辑可根据需求扩展,覆盖所有可能修改集合的方法。
内容的提问来源于stack exchange,提问作者Jan Z.
相关产品推荐
相关产品推荐

