Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NSFetchedResultsController does not read updated derived value in CoreData entity after RestKit EntityMapping update

I have a view controller where I create an NSFetchedResultsController to display a set of CoreData objects in a TableView. While viewing these objects in the tableview, they are updated by calling a RestKit getObjectsAtPath, which processes correctly through a response descriptor and RKEntityMapping to update a field on the CoreData objects. However, this particular entity also have a custom derived field - actually a state machine (TransitionKit based) that reads the state value provided to the entity and re-initializes the state machine with the state provided by the server. However, no matter where I reinitialize the state machine (awakeFromFetch, willSave, key-value observing), this reinitialized state machine is not updated when the copy of the object in the NSFetchedResultsController is used to update the corresponding table cell (when the NSFetchResultsController is notified of the change on that row). To be clear - the value that is updated via the RestKit EntityMapping IS updated, but the state machine (a derived value) is not updated. WHY would this be?

Shouldn't the NSFetchedResultsController's array of objects be notified in a way that allows them to compute their derived values? When I trace in the code, awakeFromFetch in the main thread does not yet contain the updated value, and computing my derived value in willSave or a setter does not seem to create this derived value in the instance of the object that is held by the NSFetchedResultsController.

I've attached my base model code

#import "VCStateMachineManagedObject.h"

@interface VCStateMachineManagedObject ()

@property (nonatomic, strong) TKStateMachine * stateMachine;

@end



@implementation VCStateMachineManagedObject

@dynamic savedState;
@synthesize stateMachine = _stateMachine;
@synthesize forcedState;

-(id)init {
    self = [super init];
    if(self != nil) {
    }
    return self;
}


- (BOOL)canFireEvent:(id)eventOrEventName {
    return [_stateMachine canFireEvent:eventOrEventName];
}

- (BOOL)fireEvent:(id)eventOrEventName userInfo:(NSDictionary *)userInfo error:(NSError **)error{
   return [_stateMachine fireEvent:eventOrEventName userInfo:userInfo error:error];
}

- (void) assignStatesAndEvents:(TKStateMachine *) stateMachine {
    [NSException raise:@"Invoked abstract method" format:@"Invoked abstract method"];
}

- (NSString *) getInitialState {
    [NSException raise:@"Invoked abstract method" format:@"Invoked abstract method"];
    return nil;
}


- (void)awakeFromInsert {
    if(self.savedState == nil){
        self.savedState = [self getInitialState];
    }
    [self createStateMachine];
}

- (void)awakeFromFetch {
    if(self.savedState == nil){
        self.savedState = [self getInitialState];
    }
    [self addObserver:self forKeyPath:@"savedState" options:NSKeyValueObservingOptionNew context:nil];
    [self createStateMachine];
}


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if(![[_stateMachine.currentState name] isEqualToString:self.savedState]){
        [self createStateMachine];
    }
}

- (void) willSave {
    NSLog(@"%@", self.savedState);
    [self createStateMachine];
}


// Manually set the state, for restkit object mapping
- (void) setForcedState: (NSString*) state__ {
    self.savedState = state__;
}

- (void) setSavedState:(NSString *)savedState__{
    [self willChangeValueForKey:@"savedState"];
    [self setPrimitiveValue:savedState__ forKey:@"savedState"];
    [self didChangeValueForKey:@"savedState"];
    [self createStateMachine];
}

- (NSString *) state {
    NSString * state = [_stateMachine.currentState name];
    return [NSString stringWithFormat:@"%@ %@", state, self.savedState];
}


#pragma mark - State Machine

- (void)prepareStateMachine {
    for(TKEvent * event in _stateMachine.events){
        [event setDidFireEventBlock:^(TKEvent *event, TKTransition *transition) {
            self.savedState = transition.destinationState.name;
        }];
    }
}

- (void) createStateMachine {
    _stateMachine = [TKStateMachine new];
    [self assignStatesAndEvents:_stateMachine];
    [self prepareStateMachine];
    _stateMachine.initialState = [_stateMachine stateNamed:self.savedState];
    [_stateMachine activate];
}


@end

@quellish Here's what I see when I trace breakpoints in my managed object.

  1. I call out to restkit to download new objects
  2. Restkit downloads objects in a background thread (not the main thread). I see it find the matching object (awakeFromFetch), update the state (setForcedState), and save (willSave), and on this instance of the object I see the createStateMachine method called several times (which is because I have it in all these functions, though that of course has no effect on the instance in the NSFetchedResultsController).
  3. I then see an object on the main thread get fetched (awakeFromFetch) and go through the same process.
  4. I then see an object on the main thread get triggered by KVO and again go through the createStateMachine method.

in all causes when I view variables in the state machine, they have been updated to the correct value. but THEN, when NSFetchedResultsController triggers on the change, the state machine IS NOT updated even though the value itself is.

In the next few hours I'm going to trace back through this and give even more specific information about the behavior I'm seeing. I'm also going to add a UUID to each instance of the entity to make sure that the one that is being updated is actually the one that's in the NSFetchedResultsController. Stay tuned.

like image 549
deepwinter Avatar asked Jun 28 '14 04:06

deepwinter


1 Answers

The implemention you provided in your question has a few issues. Your accessor and KVO observation implementations are working against you and seem to be interfering with Core Data's change tracking, which is KVO based.

Here are some of the things you can improve that may fix the issue you're seeing. This will definitely help some issues you may not have run into yet, but most certainly will.

observeValueForKeyPath:ofObject:change:context: A correct implementation should check the context pointer against the known value you passed to addObserver:forKeyPath:context: and removeObserver:forKeyPath:context:. This allows you to differentiate your observations from others. Which leads to the next point- A correct implementation calls super. If the context value is NOT your known value, defer to super. Example:

Adding the observer:

[self addObserver:self forKeyPath:keyPath options:options context:(__bridge void*)self];

Removing the observer:

[self removeObserver:self forKeyPath:keyPath context:(__bridge void*)self];

observeValueForKeyPath:ofObject:change:context: implementation:

- (void) observeValueForKeyPath: (NSString *) keyPath ofObject: (id) object change: (NSDictionary *) change context: (void *) context {
    if ((__bridge id)context == self){
        // This is our observation, handle it here.
                [self setStateMachine:nil];
    } else {
        // This is important for Core Data to work correctly. 
        [super observeValueForKeyPath: keyPath ofObject: object change: change context: context];
    }
}

CoreData uses KVO extensively to track changes to managed object snapshots. When using KVO with Core Data you should be aware of this, and be careful not to implement KVO correctly for Core Data - which is slightly different than for other objects. For example, automatic KVO notifications are OFF by default for modeled properties of NSManagedObject subclasses. This means that if a property is backed by a modeled attribute, it will not, by default, emit external KVO notifications. Properties that do not exist in the model will.

To enable automatic KVO notifications for a modeled property, implement a class method using the following pattern:

+ (BOOL) automaticallyNotifiesObserversFor<PropertyName> {
    return YES;
}

Where is the name of the modeled property (i.e. automaticallyNotifiesObserversForSavedState).

In your case you have chosen to implement custom accessors for your modeled property. It's not clear why you chose to do this from the code you posted (perhaps your saw the scary warning in the willSave: documentation about recursion - your will/didChangeValueForKey messages reintroduce this). It's very rare that it is necessary to provide your own accessor implementation for a managed object subclass. Normally Core Data provides an accessor for @dynamic properties at runtime. When it does so, it provides an implementation that has correct memory management and change tracking, as well as optimizations for CPU and memory.

Primitive accessor methods are for accessing the attributes of a managed object. This essentially means an instance variable backed directly by the value from the data model. Accessing attributes as primitive values is not recommended, and very rarely worthwhile. Always prefer the property accessors to get correct behavior from Core Data and your model objects.

  • Fix your KVO implementation using the guidance above.
  • Don't override init on managed object subclasses. init isn't the designated initialzier.
  • Move from implementing your own accessor for your modeled property to allowing Core Data to provide the implementation. This should be as simple as removing your current accessor implementation.
  • If you change your NSKeyValueObservationOptions to include NSKeyValueObservingOptionInitial, you will get a KVO notification for the initial value of the observed key path. In your case, this would be an opportunity to also set an intial state for your state machine.
  • Since you're implementing some kind of state machine, it is probably a good idea to allow KVO to manage your dependancies between values (i.e. between forcedState, savedState, and stateMachine). For example, set savedState as a dependant keypath of forcedState, so that when savedState changes the system knows that forcedState should be "dirty" and needs recalculation:

    + (NSSet *)keyPathsForValuesAffectingValueForForcedState {
    return [NSSet setWithObject:@"savedState"];
    

    }

  • Updating your state machine from the managed object life cycle methods will probably not be necessary once your KVO implementation is fixed. If you do decide to stick with life cycle methods, see if awakeFromSnapshotEvents: is more suited to your needs.

Since you don't seem to be mutating your state machine much at all, just recreating it, your use case is pretty simple. If I am reading your class correctly, all you need to do is set stateMachine to nil when KVO tells you that savedState has changed. If it is nil when state is accessed, call your create method to set the property. Most transient properties are implemented this way.

like image 192
quellish Avatar answered Oct 23 '22 20:10

quellish