Search code examples
objective-cobjective-c-runtimeobjective-c-category

KVO-compliant dependent readonly property in category


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:

  1. Either registering as observer for the relevant key paths or
  2. Registering for notifications of the responsible 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

Solution

  • 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.