I have a class that I need to extend by a (readonly) property exposing a collection. That collection is not backed by an instance variable. Instead it contains a filtered subset of elements of another collection declared on the same class. I need that subset property to be KVO compliant so I can bind to it. I can't manipulate or subclass my particular class to achieve that, so I need to accomplish my goal in a category.
In the case of a non-collection property depending on a non-collection property it would be very simple to make it KVO compliant even in a category. I could just implement
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForDependentProperty {
return [NSSet setWithObject:@"originalProperty"];
}
However where Apple suggests that, the documentation also states: "The keyPathsForValuesAffectingValueForKey:
method does not support key-paths that include a to-many relationship." Indeed it doesn't. And how should it? The KVO framework can't know in which way and when a dependent collection will change when the original collection is altered. For that problem Apple than proposes two solutions:
NSManagedObjectContext
instance in case of using Core Data.Since I am using a category both options seem to be inapplicable. Where would I register and (safely) unregister? I can't just start replacing methods of the base class like - (instancetype)init;
or dealloc
, and I can't extend them because I'm not in a subclass, so no calls to super
. What is the best way to achieve this? Method swizzling? Mixing in some dirty lowland C code I haven't heard about yet calling to the Obj-C runtime? Something very obvious I've overlooked? To keep things simple: In my case it would suffice if I can send just willChangeValueForKey:
and didChangeValueForKey:
messages for my dependent property on every change of the original property, no matter the kind of change.
FinalClass.h
:
@interface FinalClass : ...
@property NSSet *someCollection;
...
@end
FinalClass+Category.h
@interface FinalClass (Category)
@property (readonly) NSSet *someCollectionSubset;
@end
FinalClass+Category.m
@implementation FinalClass (Category)
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForSomeCollectionSubset {
// Not working here because the key path contains a to-many relationship
return [NSSet setWithObject:@"someCollection"];
}
- (NSSet *)someCollectionSubset {
// Change notifications for that subset needed
// No mutations needed, primitive change notifications would do
return [self.someCollection filteredSetUsingPredicate:...];
}
@end
Thanks @TheDreamsWind, I've found a very simple answer:
Simple solution
Even though the signature of the general method for automatic KVO updates for dependent keys is keyPathsForValuesAffectingValueForKey:
, the key-specific signature the KVO framework is looking for is keyPathsForValuesAffecting<Key>
and not keyPathsForValuesAffectingValueFor<Key>
. After I changed my method name accordingly, automatic KVO updates started to work with me.
However that is supposed to work only when the dependency as a whole changes. In my case it also creates simple change messages without set mutations when members of the original collection changes, even though it's not supposed to do so as I understand the documentation.
Complex solution
Another solution I found is swizzling the four change message methods willChangeValueForKey:
, didChangeValueForKey:
, willChangeValueForKey:withSetMutation:usingObjects:
and didChangeValueForKey:withSetMutation:usingObjects:
with new implementations, that call the original implementation, as well as generate an additional change message for the dependent key if needed:
- (void)willChangeValueForKeySwizzled:(NSString *)key {
// Call the original implementation
[self willChangeValueForKeySwizzled:key];
// Send additional change messages if applicable and needed
if ([FinalClass.keyPathsForValuesAffectingValueForDependentProperty containsObject:key]) {
[self willChangeValueForKey:NSStringFromSelector(@selector(dependentProperty))];
}
}
...
That approach could be used to generate also set mutation change messages for better efficiency. In my case, that efficiency gain would mean premature (over)optimizing, so I stick with the simple solution proposed.