NSFetchedResultsController Does Not Read Updated Derived Value in CoreData After Updating RestKit EntityMapping

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

Should you notify the array of NSFetchedResultsController objects so that they can calculate their derived values? When I track the code, awakeFromFetch in the main thread does not yet have the updated value, and calculating my derived value in willSave or the setter does not seem to create this derived value in the instance of the object that is stored by NSFetchedResultsController.

I attached the 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 This is what I see when I track breakpoints in a managed entity.

  • I call restkit to load new objects
  • Restkit loads objects in the background thread (not the main thread). I see that it finds the corresponding object (awakeFromFetch), updates the state (setForcedState) and saves (willSave), and in this instance of the object I see the createStateMachine method, which is called several times (this is because I have it in all these functions, although this, of course, does not affect the instance in NSFetchedResultsController).
  • Then I see that the object in the main thread receives (awakeFromFetch) and goes through the same process.
  • Then I see that the object in the main thread is started by KVO and the createStateMachine method passes again.

in all cases when I look at variables in the destination machine, they have been updated to the correct value. but THEN, when the NSFetchedResultsController triggers the changes, the state machine is NOT updated, even if this value matters.

In the next few hours, I’m going to track this and give even more specific information about the behavior that I see. I am also going to add a UUID for each instance of the object to make sure that the one that is being updated is actually the one that is in the NSFetchedResultsController. Stay with us.

+6
source share
1 answer

There are several issues with the implementation you provided in your question. Your access providers and KVOs are working against you and seem to be interfering with KVO-based master data change tracking.

Here are some of the things you can improve that can solve the problem you are seeing. This will certainly help some of the problems that you may not have encountered yet, but certainly will.

observeValueForKeyPath:ofObject:change:context: correct implementation should check the context pointer for a known value that you passed to addObserver:forKeyPath:context: and removeObserver:forKeyPath:context: This allows you to distinguish your observations from others. This leads to the next point - Proper implementation calls super. If the context value is NOT your known value, set it aside for super. Example:

Adding an observer:

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

Removing an 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 makes extensive use of KVO to track changes to managed object snapshots. When using KVO with Core Data, you should be aware of this and be careful not to correctly implement KVO for Core Data, which is slightly different from other objects. For example, automatic KVO notifications are disabled by default for simulated properties of NSManagedObject subclasses. This means that if a property is supported by a model attribute, it will by default not generate external KVO notifications. Properties that do not exist in the model will be.

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

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

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

In your case, you have chosen to implement custom accessories for your model property. It is not clear why you decided to do this from the code you posted (you may have seen a scary warning in the willSave: documentation willSave: about recursion - your messages will / didChangeValueForKey re-enter this). It is very rarely necessary to provide your own access implementation for a subclass of a managed entity. Typically, Core Data provides an accessory for @dynamic properties at runtime. When he does this, he provides an implementation that has proper memory management and change tracking, as well as optimizations for CPU and memory.

Primitive access methods are designed to access the attributes of a managed entity. This essentially means an instance variable supported directly by the value from the data model. Access to attributes as primitive values ​​is not recommended and is very rarely worth it. Always prefer that property accessors get the right behavior from Core Data and your model objects.

  • Correct the implementation of KVO using the above guide.
  • Do not override init with subclasses of managed objects. init not a designated initializer.
  • Go from implementing your own accessor to your model property to provide Core Data with an implementation opportunity. It should be as simple as deleting the current access implementation.
  • If you change NSKeyValueObservationOptions to include NSKeyValueObservingOptionInitial, you will receive a KVO notification for the initial value of the observed key path. In your case, it will also be an opportunity to set the initial state for your state machine.
  • Since you are implementing some kind of state machine, it is probably recommended to allow KVO to manage your dependencies between values ​​(for example, between forcedState, savedState and stateMachine). For example, set saveState as the dependent key path of the forcedState, so that when saveState changes, the system knows that the forced state must be dirty and needs to be recalculated:

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

    }

  • Updating your final machine from managed object lifecycle methods will probably not be necessary after fixing your KVO implementation. If you decide to stick with your lifecycle methods, see If awakeFromSnapshotEvents: More suited to your needs.

Since you do not seem to be mutating your state machine too much by simply recreating it, your precedent is pretty simple. If I read your class correctly, all you have to do is set stateMachine to nil when KVO tells you that savedState has changed. If it is zero when state is available, call the create method to set the property. Most transient properties are implemented in this way.

+2
source

Source: https://habr.com/ru/post/971450/


All Articles