Search code examples
objective-ccocoacore-datacocoa-bindings

Binding to a relations property in Core Data


I'm new in Core Data, and i got a problem i can't get my head around how to do "the right way"

I'll try and examplify my problem.

I got a entity Car. And a list of all the cars in my program. The cars have some attributes, but they are not predefined. So for each car i want to be able to define some properties. Therefore i have defined a new entity CarProperty, with a one to many relation with the car.

In the nscollectionview i would like to show some of the properties from the car, more specefic the number of kilometer (numKm) it has driven (if that property exist). So i want to bind it to a label. But how to do?

I can't say representedObject.properties.numKm, or representedObject.numKm.

How should I get around this?

Hope it makes sense.


Solution

  • This isn't an easy problem. The thing is, Core Data doesn't know anything about numKm as a property. How is it supposed to know that numKm corresponds to a particular CarProperty object?

    The fundamental problem you're describing is key-value coding compliance. Cocoa's going to look for a method called numKm on the properties object. Not finding one, it'll try sending [properties valueForKey:@"numKm"]; Since valueForKey: doesn't know what to do with numKm, you get an error, but not before it calls [properties valueForUndefinedKey:@"numKm"]

    But here's the catch: properties is an NSSet generated by Core Data, so you can't subclass it to override valueForUndefinedKey:. What you can do is create your own object that's KVC-compliant for your arbitrary properties and use that instead.

    One solution is to subclass NSDictionary and make it act as a proxy. The primitive methods are count, objectForKey: and keyEnumerator. If you override these three methods, you can create an NSDictionary that's linked to your Car object and returns the appropriate CarProperty objects. For example:

    @interface PropertyProxy : NSDictionary {
    }
    @property (nonatomic, readonly, assign) Car *car;
    
    - (id)initWithCar:(Car *)car
    
    @end
    
    @implementation PropertyProxy
    
    @synthesize car = _car;
    
    - (id)initWithCar:(Car *)car {
        if (!(self = [super init]))
            return nil;
    
        _car = car;
    
        return self;
    }
    
    - (NSInteger)count {
        return [car.properties count];
    }
    
    - (id)objectForKey:(NSString *)key {
        return [[car.properties filteredSetUsingPredicate:[NSPredicate predicateWithFormt:@"key == %@", key]] anyObject];
    }
    
    - (NSEnumerator *)keyEnumerator {
        return [[car valueForKeyPath:@"properties.key"] objectEnumerator];
    }
    
    @end
    

    Then, in your Car class, do this:

    @interface Car : NSManagedObject {
        // other stuff
    }
    
    @property (nonatomic, readonly) NSDictionary *carProperties;
    
    // other stuff
    
    @end
    
    @implementation Car
    
    // other stuff
    
    - (NSDictionary *)carProperties {
        return [[[PropertyProxy alloc] initWithCar:self] autorelease];
    }
    
    @end
    

    (Disclaimer: I just typed this into my web browser, so no guarantees this actually compiles :-))

    As you can see, it's not the easiest thing in the world to do. You'll be able to set up key paths like this:

    representedObject.carProperties.numKm;
    

    Keep in mind that, while this is key-value coding compliant, it is not key-value observing compliant. So if numKm changes, you won't be able to observe that. You would need to do some extra work to make that happen.