I am trying to clean up some transient property recalculation events. In development I have just used a very broad stroke of updating on almost all changes. Now I am trying to determine the best practice, checking for relevant keys and updating as little as needed. My application objects are loaded with dozens of calculations using child and parent attributes where a single change can result in many cascading recalculations.
I thought I understood fairly well, referring to Apple Documentation here and I have experimented with patterns seen on some other StackOverflow posts such as this one by #Marcus S. Zarra. I see Marcus also posted an article on the subject here.
What I have currently is this:
#pragma mark _ worksheetTotal (transient)
+(NSSet *)keyPathsForValuesAffectingWorksheetTotal
{
// local, non-transient, dependent keys here
return [NSSet setWithObjects:#"dailySustenance", nil];
}
-(void)updateWorksheetTotal
{
NSDecimalNumber *lineItemTotal = [self.items valueForKeyPath:#"#sum.lineHoursCost"];
NSDecimalNumber *total = [lineItemTotal decimalNumberByAdding:self.dailySustenance];
[self setWorksheetTotal:total];
}
-(void)setWorksheetTotal:(NSDecimalNumber *)newWorksheetTotal
{
if ([self worksheetTotal] != newWorksheetTotal) {
[self willChangeValueForKey:#"worksheetTotal"];
[self setPrimitiveWorksheetTotal:newWorksheetTotal];
[self didChangeValueForKey:#"worksheetTotal"];
}
}
-(NSDecimalNumber *)worksheetTotal
{
[self willAccessValueForKey:#"worksheetTotal"];
NSDecimalNumber *result = [self primitiveWorksheetTotal];
[self didAccessValueForKey:#"worksheetTotal"];
return result;
}
I am thinking this is straight out of the documentation use case but my observers are not getting notified of the the changes to worksheetTotal.
When a change is made to a lineItem, that notification is received and responds updateWorksheetTotal is called here. I expect this would trigger another notification by the context that worksheetTotal has now changed, but it does not. I have tried numerous variations and combinations of code I have seen but nothing I do seems to make the NSManagedObjectContext consider my update to worksheetTotal to be a change worth reporting to observers.
What am I missing here?
Here is the relevant code for listening for the change in Parent object.
- (void) objectContextDidChange: (NSNotification *) notification
{
NSSet *updatedObjects = [[notification userInfo] objectForKey:NSUpdatedObjectsKey];
// Next look for changes to worksheet that may affect our calculated fields
NSPredicate *worksheetPredicate = [NSPredicate predicateWithFormat:#"entity.name == %# && estimate == %#", #"LaborWorksheet", self];
NSPredicate *materialsPredicate = [NSPredicate predicateWithFormat:#"entity.name == %# && estimate == %#", #"MaterialLineItems", self];
NSPredicate *combinedPredicate = [NSCompoundPredicate orPredicateWithSubpredicates:[NSArray arrayWithObjects:worksheetPredicate, materialsPredicate, nil]];
NSSet *myWorksheets = [updatedObjects filteredSetUsingPredicate:combinedPredicate];
// These are the relevant keys in this estimates labor and materials worksheet
NSSet *relevantKeys = [NSSet setWithObjects:#"worksheetTotal", #"totalEquipmentcost", nil];
BOOL clearCache = NO;
if (myWorksheets.count != 0) {
for (NSManagedObject *worksheet in myWorksheets) {
NSLog(#"got some");
// Here is where it fails, worksheetTotal is not in changedValues.allKeys. It is an empty set.
// Not sure how that could be when I got it from updatedObjects set of userInfo. How could it have no changed values?
NSSet *changedKeys = [NSSet setWithArray:worksheet.changedValues.allKeys];
if ([changedKeys intersectsSet:relevantKeys]) {
clearCache = YES;
NSLog(#"found some, set to clear cache");
// no need to continue checking
break;
}
}
}
if (clearCache) {
// I would update dependent keys here but it is never reached
}
}
The documentation for the changedValues method says:
This method only reports changes to properties that are defined as
persistent properties of the receiver, not changes to transient
properties or custom instance variables. This method does not
unnecessarily fire relationship faults.
Since your property is transient, it will not appear there. I would suggest using KVO to monitor the changes in these properties.
You are returning before didAccessValueForKey. Store to a variable and return it after didAccessValueForKey like
-(NSDecimalNumber *)worksheetTotal
{
[self willAccessValueForKey:#"worksheetTotal"];
NSDecimalNumber *yourVariable = [self primitiveWorksheetTotal];
[self didAccessValueForKey:#"worksheetTotal"];
return yourVariable;
}
I hope it works.
Edit:
In
-(void)setWorksheetTotal:(NSDecimalNumber *)newWorksheetTotal
consider changing
if ([self worksheetTotal] != newWorksheetTotal) {
to
if (worksheetTotal != newWorksheetTotal) {
Related
I have an entity (TestEntity) which contains a "Transformable" attribute which holds an object (MyObjectClass). On initial load, the transformable saves correctly; initialised as below:
TestEntity *test = (TestEntity *)[NSEntityDescription insertNewObjectForEntityForName:ENTITY[<Int>] inManagedObjectContext:temporaryContext];
test.transformableAttr = [[MyObjectClass alloc] initWithObject:obj];
However, when I fetch an object (I fetch as dictionary with NSDictionaryResultType) and update its "Transformable" attribute,
MyObjectClass *my_obj = ....
dict[#"transformableAttr"] = my_obj
it saves successfully but when I fetch it again I get nil for the "Transformable" attribute.
Now this only happens with "NSBatchUpdateRequest" because when I save using the MOC
TestEntity *test = ....
test.transformableAttr = updated_object
it saves successfully and I can access the updated attribute when fetched again.
Can anyone please explain? Does it mean that NSBatchUpdateRequest does not Transformable?
My NSBatchUpdateRequest code:
[context performBlock:^{
NSError *requestError = nil;
NSBatchUpdateRequest *batchRequest = [NSBatchUpdateRequest batchUpdateRequestWithEntityName:entity];
batchRequest.resultType = NSUpdatedObjectIDsResultType;
batchRequest.propertiesToUpdate = properties;
NSBatchUpdateResult *result = nil;
SET_IF_NOT_NIL(batchRequest.predicate, predicate)
#try {
result = (NSBatchUpdateResult *)[context executeRequest:batchRequest error:&requestError];
if (requestError != nil){
// #throw
}
if ([result.result respondsToSelector:#selector(count)]){
__block NSInteger counter = [result.result count];
if (counter > 0){
[managedObjectContext performBlock:^{
for(NSManagedObjectID *objectID in result.result){
NSError *faultError = nil;
NSManagedObject *object = [managedObjectContext existingObjectWithID:objectID error:&faultError];
if (object && faultError == nil) {
[managedObjectContext refreshObject:object mergeChanges:YES];
}
counter--;
if (counter <= 0) {
// complete
}
else{
// Wait.
}
}
}];
}
else{
// No Changes
}
}
else {
// No Changes
}
}
#catch (NSException *exception) {
#throw;
}
}];
The documentation doesn't seem to call out this particular scenario, but I'm not surprised that it doesn't work. An NSBatchUpdateRequest is described as [emphasis mine]:
A request to Core Data to do a batch update of data in a persistent store without loading any data into memory.
Transformables work by converting to/from Data in memory. If your class conforms to NSCoding, the coding/decoding happens in memory, because SQLite doesn't know about NSCoding or your classes.
Your original assignment works because Core Data converts the value of transformableAttr to Data in memory and then saves the bytes of the Data to the persistent store. In the batch update, the objects aren't loaded into memory, so the transformation can't run, so the update doesn't work as you'd expect.
It's disappointing that Core Data doesn't make this clearer. Look in the Xcode console to see if it warns you about this. If it doesn't, please file a bug with Apple, because though I don't expect this to work, it's also not good for it to fail silently.
If you want to use batch updates, you'll have to convert your value in code before running the update. I'm not 100% certain how this will work but if your value conforms to NSCoding you'll start with
let transformedData: Data = NSKeyedArchiver.archivedData(withRootObject:transformableAttr)
What you do then is where I'm not sure. You might be able to use transformedData as the new value. Or you might have to access its bytes and use them somehow-- maybe using withUnsafeBytes(_:). You'll probably run into trouble because transformableAttr is not a Data, so it may get messy. It seems that batch updates aren't designed to work well with transformables.
I've finished implementing a swipe feature similar to tinder but running into problems when saving objects.
I have 2 columns in the currentUser's row in my DB. One column hold an array of acceptedUsers (users have been liked) and the other is a rejectedUsers column that holds an array of rejected users (users that have been left swiped).
This is how my DB is updated upon swipe:
-(void)cardSwipedLeft:(UIView *)card;
{
NSString *swipedUserId = [[[userBeingSwipedArray objectAtIndex:0] valueForKey:#"user"] valueForKey:#"objectId"];
[currentUserImagesRow addUniqueObject:swipedUserId forKey:#"rejectedUsers"];
[currentUserImagesRow saveInBackground];
This works fine when I left about 2+ seconds between swipes. However fast swiping causes some saves to fail.
Is there a better way to do this without spoiling the users experience of the app?
I've saved multiple rows to my database before using for loops and this has always worked for me. I thought parse.com would be able to handle the speed of the saving.
I'm using both swift and objective-c for this project.
Thanks for your time
Its a fun problem. I think the way to go is to decouple the swiping and the saving a little bit more. Start with a collection of what needs saving...
#property(nonatomic, strong) NSMutableArray *toSave;
#property(nonatomic, assign) BOOL busySaving;
// on swipe
[self.toSave addObject: currentUserImagesRow];
[self doSaves];
- (void)doSaves {
// we get called because of user interaction, and we call ourselves
// recursively when finished. keep state so these requests don't pile up
if (self.busySaving) return;
if (self.toSave.count) {
self.busySaving = YES;
[PFObject saveAllInBackground:self.toSave block:^(BOOL succeeded, NSError *error) {
self.busySaving = NO;
// remove just the elements that were saved, remaining aware that
// new ones might have arrived while the last save was happening
NSMutableArray *removes = [#[] mutableCopy];
for (PFObject *object in self.toSave) {
if (!object.isDirty) [removes addObject:object];
}
[self.toSave removeObjectsInArray:removes];
[self doSaves];
}];
}
}
Now, instead of processing single saves, we can handle small batches. A user swipe causes a single save, and we block additional requests until the current one is complete. During the current request, we let more saves queue up as the user continues to interact. We call ourselves recursively after a save in case one or more records were queued. If none were, the recursive call ends immediately.
EDIT - Saving just one object is easier, just do the same blocking trick and recursive call at the end, but no need to track or save batches...
#property(nonatomic, assign) BOOL busySaving;
// on swipe
[self doSaves];
- (void)doSaves {
if (self.busySaving) return;
if (currentUserImagesRow.isDirty) {
self.busySaving = YES;
[currentUserImagesRow saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
self.busySaving = NO;
[self doSaves];
}];
}
}
Danh led me to my answer. There was an issue on my side which was causing the problem mentioned in the comments above. The same ID was being used after each swipe because I didn't remove it from the array that held my user objects. Below is how I solved the issue and used the Danh's answer to find my solution.
-(void)cardSwipedRight:(UIView *)card
{
NSString *swipedUserId = [[[userBeingSwipedArray objectAtIndex:0] valueForKey:#"user"] valueForKey:#"objectId"];
// I save the swiped users id and the key for the column it will
// be saved in e.g. if liked then "acceptedUsers" if notLiked then
// "rejectedUsers" so that the doSave method saves to correct column
NSArray *array = #[swipedUserId, #"acceptedUsers"];
[self.toSave addObject: array];
//remove user from array since he/she is now saved
[userBeingSwipedArray removeObjectIdenticalTo:[userBeingSwipedArray objectAtIndex:0]];
[self doSaves];
and then:
- (void)doSaves {
if (self.busySaving) return;
if (self.toSave.count) {
self.busySaving = YES;
NSArray *arrayWithSwipedUsersIdAndKeyForColumn = [self.toSave objectAtIndex:0];
[currentUserImagesRow addUniqueObject:[arrayWithSwipedUsersIdAndKeyForColumn objectAtIndex:0] forKey:[arrayWithSwipedUsersIdAndKeyForColumn objectAtIndex:1]];
[currentUserImagesRow saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
self.busySaving = NO;
//remove object that was just saved seeing as it is no longer needed
[self.toSave removeObjectIdenticalTo:arrayWithSwipedUsersIdAndKeyForColumn];
[self doSaves];
}];
}
}
Saving now works 100% of the time providing there is an internet connection. I can swipe as fast as I wish and objects always get saved.
I am starting in Unit testing with objective-c and I need to know how to test blocks with OCMockito and Xcode 6.
I am testing an Interactor, this interactor should return an array as a block argument and I has to ask the Provider file for the elements.
This is the method I want to test:
- (void)userPoiListsWithSuccessBlock:(MNBSavePoisInteractorSuccess)success {
self.poiListEntityArray = [self.poiListProvider poiListsForUser:self.loggedUser];
self.poiListViewObjectArray = [self viewPoiListObjectListWithPoiLists:self.poiListEntityArray];
success(self.poiListViewObjectArray);
}
First, I setup the elements that I am going to use
self.mockPoiListProvider = mock([PoiListProvider class]);
self.sut = [[MNBSavePoisInteractor alloc] initWithManagedObjectContext:self.coreDataStack.managedObjectContext andPoiListProvider:self.mockPoiListProvider];
- (UserEntity *)loggedUserMock {
UserEntity *mockLoggedUser = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([UserEntity class]) inManagedObjectContext:self.coreDataStack.managedObjectContext];
mockLoggedUser.userId=#"1";
mockLoggedUser.username=#"user";
mockLoggedUser.loggedUser=#YES;
return mockLoggedUser;
}
- (InMemoryCoreDataStack *)coreDataStack{
if (!_coreDataStack) {
_coreDataStack = [[InMemoryCoreDataStack alloc] init];
}
return _coreDataStack;
}
- (PoiListEntity *)poiListFake {
PoiListEntity *fake = [NSEntityDescription insertNewObjectForEntityForName:#"PoiListEntity" inManagedObjectContext:self.coreDataStack.managedObjectContext];
fake.name = #"Test";
fake.poisCount = #2;
[fake addContributorsObject:[self loggedUserMock]];
return fake;
}
Then, I do the test. I am using Xcode 6 waitForExpectation to manage the asynchronous methods. I think I am doing something wrong.
- (void)waitForExpectation {
[self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) {
if (error) {
NSLog(#"Timeout Error: %#", error);
}
}];
}
- (void)testShouldReturnPoiLists {
XCTestExpectation *expectation = [self expectationWithDescription:#"Waiting method ends"];
[given([self.mockPoiListProvider poiListsForUser:[self loggedUserMock]]) willReturn:#[[self poiListFake]]];
[self.sut userPoiListsWithSuccessBlock:^(NSArray *results) {
[expectation fulfill];
XCTAssert(resutls.count == 1, #"Results %zd", resutls.count);
}];
[self waitForExpectation];
}
I understood if I give the object in willReturn in the given method, when I call the sut method that I want to test it should return what I give before. Is that true?
Thank you
I see no asynchronous code. You just want a block that captures the results, so use a __block variable to make the results available outside of the block. Then you can assert whatever you want:
- (void)testShouldReturnPoiLists {
[given([self.mockPoiListProvider poiListsForUser:[self loggedUserMock]]) willReturn:#[[self poiListFake]]];
__block NSArray *capturedResults;
[self.sut userPoiListsWithSuccessBlock:^(NSArray *results) {
capturedResults = results;
}];
assertThat(capturedResults, hasLengthOf(1));
}
The relationship between the length of 1 and the fake is hard to tell. Let's also parameterize the faking code:
- (PoiListEntity *)poiListFakeWithName:(NSString *)name count:(NSNumber *)count {
PoiListEntity *fake = [NSEntityDescription insertNewObjectForEntityForName:#"PoiListEntity" inManagedObjectContext:self.coreDataStack.managedObjectContext];
fake.name = name;
fake.poisCount = count;
[fake addContributorsObject:[self loggedUserMock]];
return fake;
}
With that, we can write more interesting tests.
I do want to add that it's important to "listen to the tests." There's a lot of convoluted set-up to dance around Core Data. That tells me that if you can rewrite things to be independent of Core Data — completely ignorant of it — everything will be much simpler.
I am using what I think is a fairly typical implementation of a NSManagedObject subclass which conforms to MKAnnotation protocol so as to display in a MKMapView. See setters and getters:
-(CLLocationCoordinate2D)coordinate {
CLLocationCoordinate2D coord = EMPTY_LOCATION_COORDINATE;
BOOL validLong = (self.longitude != nil) && ([self.longitude doubleValue] != 0);
BOOL validLat = (self.latitude != nil) && ([self.latitude doubleValue] != 0);
if (validLong && validLat) {
coord.longitude = [self.longitude doubleValue];
coord.latitude = [self.latitude doubleValue];
}
return coord;
}
-(void)setCoordinate:(CLLocationCoordinate2D)coordinate {
if (coordinate.latitude != EMPTY_LOCATION && coordinate.longitude != EMPTY_LOCATION) {
self.latitude = [NSNumber numberWithDouble:coordinate.latitude];
self.longitude = [NSNumber numberWithDouble:coordinate.longitude];
} else {
self.latitude = nil;
self.longitude = nil;
}
}
-(NSString *)title {
NSString *str = [self.projectName copy];
return str;
}
This is working and not causing problems in production at all.
I was debugging some Core Data concurrency issues using Core Data multi-threading assertions and I find that it is flagging the gutter as a concurrency violation. My guess is that the MKMapview that calls for the coordinate is using a background thread and technically that is not allowed. That it works in production is, conceivably, not guaranteed.
I tried to wrap the getter in a [self.managedObjectContext performBlockAndWait^(void){ //set here }]; block but that causes thread locking fail.
Should I ignore the error and move on or is there some better practice for this purpose?
I could not find the reason for this. I verified that the NSManagedObject is on the main queue context. It is being asked for the coordinate on a queue that is not the main queue. What I did to fix is use a proxy object as annotation passed to the MKMapview instead of passing it directly. NSObject class conforming to the MKAnnotation protocol. Initialize with the coordinate and title from my NSManagedObject, pass that instead of the real deal.
First, the coordinate should be implemented as a transient including a magic keyPathsForValuesAffectingCoordinate method. You can start with an uncached transient for simplicity and then perhaps add the additional code to cache it if necessary.
Second, validation should be done using Core Data.
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreData/ObjectValidation.html#//apple_ref/doc/uid/TP40001075-CH20-SW1
I am not sure that there are not problems with my solution but it was way too complicated for me to make a proxy object. I have tested it and it does not seem to give a thread violation with the -com.apple.CoreData.ConcurrencyDebug option enabled. Since I know my NSManagedObjectContext is on the main queue I did:
- (NSString *) title {
if( [NSThread isMainThread] ) {
return [self.projectName copy];
} else {
__block NSString *title;
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType: NSPrivateQueueConcurrencyType];
context.persistentStoreCoordinator = self.managedObjectContext.persistentStoreCoordinator;
[context performBlockAndWait: ^{
ActivityLocation *tmp = [context objectWithID: self.objectID];
title = [tmp.projectName copy];
}];
return title;
}
}
I did the same thing for coordinate and subtitle and any other method that was causing problems.
Apparently there is some "wait" operations related to the main queue because if I set the newly created context's parent to 'self's context, it deadlocks.
Interestingly, even though the newly created context is on the thread that is needed, it does not work without the performBlockAndWait: even though if I print the current thread with or without the performBlockAndWait: it prints the same thing.
If there are unsaved changes in the object's projectName property, the old values will be displayed since the persistentStoreCoordinator is used for the newly created context, but I have control over that, unlike my lack of control for which thread Apple calls my MKAnnotation methods on.
I hope this is useful to someone, even though I am responding to a question asked over six years ago!
For the purpose of ensuring I have chosen the correct NSMergePolicy I am curious as to whether a value being set to its current value is capable of causing a merge conflict across multiple contexts.
Specifically, in my case I want to ensure that a modified flag will conflict and be preserved if set at an inopportune moment.
Example:
//...
//on background thread, doing some work to an object because it's status was
//set to Status_Modified
[managedObjectContext performBlockAndWait:^{
object.status = Status_NotModified;
[managedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
[managedObjectContext save:&error];
}];
What if while this is going on, on the main thread, the status is set to Status_Modified and saved? Will the objects status stay as Status_Modified? I.e. will the 'status' property be considered to be changed and so cause a conflict and therefore trump our in memory change (according to the policy)?
So, I cannot find any decent documentation to answer this question, but I have done some tests and it seems that the property is considered to be changed. This was my suspicion and seems to agree with various references to key-value setting being wrapped by will/didSetValueForKey:
My test:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSManagedObjectContext * uiCtx = [self contextForUIWork];
NSEntityDescription * entityDesc = [NSEntityDescription entityForName:#"Entity" inManagedObjectContext:uiCtx];
Entity * entity = [[Entity alloc] initWithEntity:entityDesc insertIntoManagedObjectContext:uiCtx];
entity.testproperty = #(1);
NSError * error = nil;
[uiCtx save:&error];
if (error)
{
assert(0);
}
NSManagedObjectID * objID = entity.objectID;
[self doStuffToObjectWithIDOnAnotherThreadAndAnotherContext:objID];
entity.testproperty = #(2);
[uiCtx setMergePolicy:NSErrorMergePolicy];
error = nil;
[uiCtx save:&error];
if (!error)
{
//We do not hit this! Success!
assert(0);
}
}
-(void)doStuffToObjectWithIDOnAnotherThreadAndAnotherContext:(NSManagedObjectID*)objID
{
dispatch_barrier_sync(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
NSManagedObjectContext * bgCtx = [self contextForBackgroundWork];
Entity * bgEntity = (Entity*)[bgCtx objectWithID:objID];
[bgCtx performBlockAndWait:^{
//set to same value
bgEntity.testproperty = bgEntity.testproperty;
NSError * bgError = nil;
[bgCtx save:&bgError];
if (bgError)
{
assert(0);
}
}];
});
}
(Full test code uploaded here: https://github.com/samskiter/CoreDataValueChangingTest )
A citation from the docs confirming this would be far better than just some test that shows it works on this particular version of iOS.