CoreData: Inverse relationships causing crash on save - ios

I have a bit of code in my application that generates a Core Data Model and populates it with a set of NSEntityDescriptions. Each of these entity descriptions then have an arbitrary number of NSPropertyDescriptions allocated for them. These properties are a combination of NSAttributeDescriptions and NSRelationshipDescriptions. All relationships are matched with an existing relationship and they are set as inverse of one another using setInverseRelationship:.
The attribute properties work fine, and to-many relationships work fine; I have tested that thoroughly. The issue seems to be with NSRelationshipDescriptions that have a maxCount value of 1, meaning the property description returns a isToMany value of NO. When the inverseRelationship property is set for these type of relationship, Core Data crashes when I try to save any object that utilizes that relationship with the error:
2013-11-09 11:17:15.068 Directory[1344:5c03] -[NSManagedObject count]: unrecognized selector sent to instance 0x9643820
2013-11-09 11:17:15.074 Directory[1344:5c03] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSManagedObject count]: unrecognized selector sent to instance 0x9643820'
*** First throw call stack:
(
0 CoreFoundation 0x01f9f5e4 __exceptionPreprocess + 180
1 libobjc.A.dylib 0x01d228b6 objc_exception_throw + 44
2 CoreFoundation 0x0203c903 -[NSObject(NSObject) doesNotRecognizeSelector:] + 275
3 CoreFoundation 0x01f8f90b ___forwarding___ + 1019
4 CoreFoundation 0x01f8f4ee _CF_forwarding_prep_0 + 14
5 CoreData 0x0083c128 -[NSSQLCore _knownOrderKeyForObject:from:inverseToMany:] + 200
6 CoreData 0x00773080 -[NSSQLCore _populateRowForOp:withObject:] + 1120
7 CoreData 0x00789157 -[NSSQLCore recordValuesForInsertedObject:] + 71
8 CoreData 0x00771e8d -[NSSQLCore recordChangesInContext:] + 685
9 CoreData 0x00770c55 -[NSSQLCore saveChanges:] + 565
10 CoreData 0x0073d88c -[NSSQLCore executeRequest:withContext:error:] + 412
11 CoreData 0x0073d380 -[NSPersistentStoreCoordinator executeRequest:withContext:error:] + 4704
12 CoreData 0x00769ffc -[NSManagedObjectContext save:] + 764
13 Directory 0x0002f2bf -[ETDatabaseController save] + 111
14 Directory 0x0002ba9c -[ETDataLoader performImportWithData:] + 10284
The insinuation I am gathering from this is it is considering the inverse relationship to be a to-many relationship when that is not the case. According to my assertions, what I know about each of my relationships is:
relationDescription.inverseRelationship != nil
[relationDescription.inverseRelationship.inverseRelationship isEqual:relationDescription]
I have tested this by creating the model and populating it with a small set of sample data. Currently, objects that have any sort of attributes, to-many relationships (with/without inverse), and to-one relationship (without inverse) work consistently. The issues comes when I try to have a to-one relationship with an inverse relationship.
This seems a bit convoluted so let me know if I need to clarify anything better. Thanks!
Edit 1:
The relationship creation is done in two steps, first it creates all the relationships, then it establishes the inverse of each relationship by using a lookup.
NSRelationshipDescription *description = [[NSRelationshipDescription alloc] init];
[description setName:self.name];
[description setDestinationEntity:entity];
[description setMaxCount:(isToMany ? 0 : 1)];
[description setMinCount:0];
[description setOrdered:YES]; // See you in a minute
later...
NSEntityDescription *inverseEntity = newRelationship.destinationEntity;
NSRelationshipDescription *inverseRelationDescription = [inverseEntity relationshipsByName][inverse.name];
if (inverseRelationDescription) {
inverseRelationDescription.inverseRelationship = newRelationship;
newRelationship.inverseRelationship = inverseRelationDescription;
} else if ([inverse.name isEqualToString:relation.name]) {
newRelationship.inverseRelationship = newRelationship;
}

Well, I guess writing Edit 1 made something click. So from what it looks like, if you call setOrdered: on a NSRelationDescription that is meant to be a to-one relationship, the internals of Core Data automatically considers it a to-many relationship, despite the conflict with the maxCount being one.
I fixed this by changing the creation code for the relationship from:
NSRelationshipDescription *description = [[NSRelationshipDescription alloc] init];
[description setName:self.name];
[description setDestinationEntity:entity];
[description setMaxCount:(isToMany ? 0 : 1)];
[description setMinCount:0];
[description setOrdered:YES];
to:
NSRelationshipDescription *description = [[NSRelationshipDescription alloc] init];
[description setName:self.name];
[description setDestinationEntity:entity];
[description setMaxCount:(isToMany ? 0 : 1)];
[description setMinCount:0];
if (isToMany)
[description setOrdered:YES];

Related

iOS Core Data dispatch_async background queuing crash

I'm getting a non-reproduceable crash and I'm not sure why. I'm caching images on a background queue. The image names are properties on a Core Data NSManagedObject subclass, CCCard. At the same time, I have a collection view that is also accessing these CCCards.
Here is the relevant code and notes to follow.
//CCDeckViewController.m --------------------------------------
- (void)viewDidLoad {
// This will fetch the CCCard objects from Core Data.
self.cards = [[CCDataManager shared] cards];
[self cacheImages];
}
- (void)cacheCardImages {
// Because these are NSManagedObjects, I access them in the background
// via their object ID. So pull the IDs out here.
NSArray *cardIds = [self.cards valueForKey:#"objectID"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
for (NSManagedObjectID *cardId in cardIds) {
// Fetch the actual CCCard object.
CCCard *card = (CCCard *)[[CCDataManager shared] objectWithId:cardId];
// Cache the card's image if it's not already there.
NSString *key = [card thumbnailCacheKey];
if ([self.cardImageCache objectForKey:key]) {
continue;
}
CCDiscardableImage *discardable = [CCHelper decompressedImageForPath:key size:[card thumbnailSize] tintable:[card tintable]];
[self.cardImageCache setObject:discardable forKey:key];
}
});
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
CCCard *card = self.cards[indexPath.item];
// This line calls the code that crashes.
UIColor *color = [card color];
// More code that returns the cell.
}
// CCCard.m --------------------------------------
- (UIColor *)color {
// Crash happens on this line.
if (self.colorId.integerValue == 0) {
// some code
}
}
Here are parts of the stack trace from the Crashlytics report I think are most relevant:
Thread #0: Crashed: com.apple.main-thread
EXC_BREAKPOINT 0x000000000000defe
0 CoreData 0x24c1b070 _sharedIMPL_pvfk_core + 247
1 CoreData 0x24c1b071 _sharedIMPL_pvfk_core + 248
2 myapp 0x4286f -[CCCard color] (CCCard.m:37)
3 myapp 0x40bbb -[CCDeckViewController collectionView:cellForItemAtIndexPath:] (CCDeckViewController.m:127)
Thread #4: com.apple.root.utility-qos
0 libsystem_pthread.dylib 0x2321920c RWLOCK_GETSEQ_ADDR
1 libsystem_pthread.dylib 0x23219295 pthread_rwlock_unlock + 60
2 libobjc.A.dylib 0x22cb8e01 rwlock_tt<false>::unlockRead() + 8
3 libobjc.A.dylib 0x22cb5af5 lookUpImpOrForward + 488
4 libobjc.A.dylib 0x22cb5903 _class_lookupMethodAndLoadCache3 + 34
5 libobjc.A.dylib 0x22cbbd7b _objc_msgSend_uncached + 26
6 CoreData 0x24c1d3ab _PFObjectIDFastEquals64 + 38
7 CoreFoundation 0x233eeed4 CFBasicHashFindBucket + 1820
8 CoreFoundation 0x233ee775 CFDictionaryGetValue + 116
9 CoreData 0x24c17037 _PFCMT_GetValue + 122
10 CoreData 0x24c16ec7 -[NSManagedObjectContext(_NSInternalAdditions) _retainedObjectWithID:optionalHandler:withInlineStorage:] + 58
11 CoreData 0x24c1b45d _PF_FulfillDeferredFault + 940
12 CoreData 0x24c1afcf _sharedIMPL_pvfk_core + 86
13 myapp 0x42991 -[CCCard imagePath] (CCCard.m:53)
14 myapp 0x41d5b __39-[CCDeckViewController cacheCardImages]_block_invoke (CCDeckViewController.m:295)
It's frustrating because it never happens during development, so can't test any theories. I'm trying to understand the code and crash report to figure out the problem now. My guess is that it has something to do with faulting because it accesses the CCCard object's instance method, but then crashes when trying to read the property from it. But why?
You are violating thread confinement.
Per the Core Data documentation, a NSManagedObjectContext and any NSManagedObject associated with it must only be accessed on the queue that it is assigned to.
Your dispatch_async violates that rule and needs to be corrected. There is no longer a design where you can use Core Data in a dispatch_async like that.
If you want the processing to be asynchronous then you should spawn a private NSManagedObjectContext that is a child of your main NSManagedObjectContext and then execute a -performBlock: against it. That will give you background processing that is configured properly.
Also, I highly recommend turning on -com.apple.CoreData.ConcurrencyDebug 1 as that will catch issues like this during development.

How to map a local JSON string into an object using RestKit 0.24 with RKObjectMapping?

I am trying to map a JSON NSString into an object using RestKit 0.24.0 where I have a RKObjectMapping as well as the corresponding dictionary.
Most of the solutions online refer to RestKit 0.22 and below.
Some of the solutions on SO result into app crash.
What is the easiest way to convert a local string into an object. Consider the following
CODE UPDATE
RKMapperOperation *mapper = [[RKMapperOperation alloc] initWithRepresentation:parsedData mappingsDictionary:[RJobObject mappingDictionary]];
RJobObject * rrrr = [[RJobObject alloc] init];
mapper.targetObject = rrrr;
//mapper.mappingOperationDataSource = parsedData;
[mapper execute:nil];
Here , mappingDictionary is basically the key-value pair matching between the JSON key and Object variable name
So in the above code whenever I run execute, the app crashes.
Stack Trace
2015-11-22 09:12:29.193 Help[22427:699579] -[__NSCFConstantString forceCollectionMapping]: unrecognized selector sent to instance 0x1022736a0
2015-11-22 09:12:29.211 Help[22427:699579] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFConstantString forceCollectionMapping]: unrecognized selector sent to instance 0x1022736a0'
*** First throw call stack:
(
0 CoreFoundation 0x0000000105d7dc65 __exceptionPreprocess + 165
1 libobjc.A.dylib 0x0000000105a14bb7 objc_exception_throw + 45
2 CoreFoundation 0x0000000105d850ad -[NSObject(NSObject) doesNotRecognizeSelector:] + 205
3 CoreFoundation 0x0000000105cdb13c ___forwarding___ + 988
4 CoreFoundation 0x0000000105cdacd8 _CF_forwarding_prep_0 + 120
5 Help 0x000000010212f424 -[RKMapperOperation mapRepresentationOrRepresentations:atKeyPath:usingMapping:] + 132
6 Help 0x000000010212fe97 -[RKMapperOperation mapSourceRepresentationWithMappingsDictionary:] + 1959
7 Help 0x00000001021307a2 -[RKMapperOperation main] + 1330
8 Foundation 0x00000001034c4774 -[__NSOperationInternal _start:] + 645
9 Help 0x00000001021310b7 -[RKMapperOperation execute:] + 39
10 Help 0x0000000101f62f8e -[AppDelegate fetchJobsFromDB] + 1102
More Details
Updated to RestKit 0.26.0 , am still facing the issue.
Basically in RKMapperOperation.m, line 333 a po mapping.forceCollectionMapping gives an error and it crashed the app.
ANSWER : Sample Solution
Use RKMappingOperation , as we do not have to specify the keyPath
RJobObject * rrrJob = [RJobObject new];
RKMappingOperation *mappingOperation = [[RKMappingOperation alloc] initWithSourceObject:jobEntity.dictionary destinationObject:rrrJob mapping:[RJobObject mapping]];
mappingOperation.dataSource = (id)rrrJob;
[mappingOperation start];
For what you're trying to do 'parsedData' should be a dictionary or array that has been deserialised from the JSON string, and [RJobObject mappingDictionary] should be a dictionary where the keys are the source key paths and the values are RKMapping Instances defining what to do with the contents.
The idea is that the key paths determine how to drill into the JSON data and the mapping determines how to process the content found there. Your key path might be an empty string if you want to process the whole JSON data.
You might consider using RKMappingOperation instead if you want to avoid the key path complexity and you're mapping the whole JSON into a single target object.

CoreData could not fulfill a fault with unmodified relationship

I have a entity A, it has a relationship B.
Here is a array fetched from CoreData:
NSPredicate * predicate = [NSPredicate predicateWithFormat:#"isSynchronized = NO"];
NSManagedObjectContext *context = [NSManagedObjectContext MR_contextForCurrentThread];
NSArray * newWordsToSynchronize = [A MR_findAllWithPredicate:predicate inContext:context];
NSMutableArray * newWordsParameters = [NSMutableArray arrayWithCapacity:[asToSynchronize count]];
[newWordsToSynchronize enumerateObjectsUsingBlock:^(A * a, NSUInteger idx, BOOL * stop) {
NSLog(#"============= may have some problem ============");
NSLog(#"[a hasFaultForRelationshipNamed:#\"b\"] = %d", [a hasFaultForRelationshipNamed:#"b"]);
NSLog(#"a.word = %#", a.word);
NSLog(#"a.b.objectID = %#", a.b.objectID);
NSLog(#"a.managedObjectContext = %#", a.managedObjectContext);
NSLog(#"a.b.managedObjectContext = %#", [a.b managedObjectContext]);
NSLog(#"a.b.managedObjectContext = %#", [a.b managedObjectContext]);
NSLog(#"a.managedObjectContext = %#", a.managedObjectContext);
NSDictionary *paramDictionary = #{
#"nbook" : a.b.name,
#"lang" : a.lang,
...
};
[newWordsParameters addObject:paramDictionary];
}];
The [a.b managedObjectContext] method may return nil when I call it at second time and then the app goes crash.
Crash log from console:
2014-01-20 14:29:14.561 MyApp[10563:2e07] a.b.objectID = 0x1754e940 <x-coredata://DFC4F956-D5AC-4264-8717-C07090CA9547/B/p1>
2014-01-20 14:29:14.561 MyApp[10563:2e07] a.managedObjectContext = <NSManagedObjectContext: 0x1754bc00>
2014-01-20 14:29:14.561 MyApp[10563:2e07] a.b.managedObjectContext = <NSManagedObjectContext: 0x1754bc00>
2014-01-20 14:29:14.562 MyApp[10563:2e07] a.b.managedObjectContext = (null)
*** Terminating app due to uncaught exception 'NSObjectInaccessibleException', reason: 'CoreData could not fulfill a fault for '0x1754e940 <x-coredata://DFC4F956-D5AC-4264-8717-C07090CA9547/B/p1>''
*** First throw call stack:
(
0 CoreFoundation 0x037495e4 __exceptionPreprocess + 180
1 libobjc.A.dylib 0x034cc8b6 objc_exception_throw + 44
2 CoreData 0x031a233b _PFFaultHandlerLookupRow + 2715
3 CoreData 0x031a1897 -[NSFaultHandler fulfillFault:withContext:forIndex:] + 39
4 CoreData 0x031a1473 _PF_FulfillDeferredFault + 259
5 CoreData 0x031a12c6 _sharedIMPL_pvfk_core + 70
6 CoreData 0x031e7130 _pvfk_5 + 32
7 MyApp 0x00181b00 __68-[CichangAHTTPEngine synchronizeAsOnCompletion:failure:]_block_invoke + 1760
8 CoreFoundation 0x037435eb __NSArrayEnumerate + 571
9 CoreFoundation 0x03743196 -[NSArray enumerateObjectsWithOptions:usingBlock:] + 102
10 CoreFoundation 0x037430a5 -[NSArray enumerateObjectsUsingBlock:] + 53
11 MyApp 0x0018105b -[CichangAHTTPEngine synchronizeAsOnCompletion:failure:] + 1179
12 MyApp 0x00185d9e __65-[CichangAHTTPEngine downloader:didFinishedDownloadObject:]_block_invoke_3 + 2030
13 libdispatch.dylib 0x040717f8 _dispatch_call_block_and_release + 15
14 libdispatch.dylib 0x040864b0 _dispatch_client_callout + 14
15 libdispatch.dylib 0x04074eeb _dispatch_root_queue_drain + 287
16 libdispatch.dylib 0x04075137 _dispatch_worker_thread2 + 39
17 libsystem_pthread.dylib 0x04412dab _pthread_wqthread + 336
18 libsystem_pthread.dylib 0x04416cce start_wqthread + 30
)
libc++abi.dylib: terminating with uncaught exception of type _NSCoreDataException
I found that B will be deallocated after first [a.b managedObjectContext] calling.
I think it was mainly caused by Core Data 1550 error (If I do not call [a.b managedObjectContext], Core Data will reports "The operation couldn’t be completed. (Cocoa error 1550.)"). But what may cause 1550 error and how to fix it?
1550 error report:
Error Domain=NSCocoaErrorDomain Code=1550 "The operation couldn’t be completed. (Cocoa error 1550.)" UserInfo=0x167269c0 {NSLocalizedDescription=The operation couldn’t be completed. (Cocoa error 1550.), Dangling reference to an invalid object.=null, NSValidationErrorObject=<A: 0x16bab420> (entity: A; id: 0x16baae50 <x-coredata://A85EE9AB-97C1-43C1-B92E-A6C906F0C1A8/A/p168> ; data: {
date = "2014-01-21 02:32:00 +0000";
expect = "2014-01-21 02:31:59 +0000";
isSynchronized = 1;
lang = en;
last = "2014-01-21 02:01:59 +0000";
level = 1;
b = "0x16bab460 <x-coredata://A85EE9AB-97C1-43C1-B92E-A6C906F0C1A8/B/p1>";
status = "-1";
studycount = 0;
trans = "v. \U6d4b\U91cf\Uff0c\U6743\U8861 ";
word = measure;
}), NSAffectedObjectsErrorKey=(
"<B: 0x16babd70> (entity: B; id: 0x16bab460 <x-coredata://A85EE9AB-97C1-43C1-B92E-A6C906F0C1A8/B/p1> ; data: <fault>)"
), NSValidationErrorKey=newWordBook, NSValidationErrorValue=<B: 0x16babd70> (entity: B; id: 0x16bab460 <x-coredata://A85EE9AB-97C1-43C1-B92E-A6C906F0C1A8/B/p1> ; data: <fault>), NSValidationErrorShouldAttemptRecoveryKey=true}
If the 1550 error happened, "CoreData could not fulfill a fault" crash will more easer to cause.
“Core Data could not fulfill fault” really means that there was a deletion. There was data in the persistent store. Then managed object representing this data was created. The object was a fault. Then the data in the store was deleted. Then some property of an object was accessed and Core Data tried to fulfill the fault, but couldn’t.
If you’re not doing explicit deletions, then also make sure that implicit deletions are not happening. Deletion behind the scenes could happen when one of your relationships was configured with the cascade deletion rule.
In your example this could happen if the relationship from A to B was configured with cascade rule, and object A was deleted.

OCMock failure mocking 'initWithAttributedString' on NSMutableAttributedString

I'm trying to understand what is mockable and what isn't.
In an experiment with an NSMutableAttributedString, I don't appear to be able to mock initWithAttributedString.
- (void)test_mutableString_shouldWorkAsAMutableString {
NSMutableAttributedString *_mutable = [OCMockObject mockForClass:NSMutableAttributedString.class];
NSAttributedString *_string = [OCMockObject mockForClass:NSAttributedString.class];
[[[(id)_mutable expect] andReturnValue:nil] initWithAttributedString:_string];
[_mutable initWithAttributedString:_string];
}
This code won't run; for some reason the proxy for the mutable screen doesn't recognise the initWithAttributedString selector:
2013-03-12 11:25:30.725 UnitTests[11316:c07] TestItClass/test_4_mutableString_shouldWorkAsAMutableString ✘ 0.00s
Name: NSInvalidArgumentException
File: Unknown
Line: Unknown
Reason: *** -[NSProxy doesNotRecognizeSelector:initWithAttributedString:] called!
0 CoreFoundation 0x01c0602e __exceptionPreprocess + 206
1 libobjc.A.dylib 0x01948e7e objc_exception_throw + 44
2 CoreFoundation 0x01c05deb +[NSException raise:format:] + 139
3 Foundation 0x00862bcd -[NSProxy doesNotRecognizeSelector:] + 75
4 CoreFoundation 0x01bf5bbc ___forwarding___ + 588
5 CoreFoundation 0x01bf594e _CF_forwarding_prep_0 + 14
6 UnitTests 0x00349e0b -[TestItClass test_4_mutableString_shouldWorkAsAMutableString] + 283
I am trying to understand how I can reliably use OCMock, but this confuses my and I'm not sure which OCMock calls I can expect to work and which I shouldn't.
I would very much appreciate some clarification on this, and an hints as to why the above doesn't work.
Thanks,
Joe
I learned something about Objective-C trying to figure this one out.
Your basic problem is that the class of an object created by alloc'ing NSMutableAttributedString is not NSMutableAttributedString (always be wary of toll-free bridged classes). To get your code to work, try the following:
NSMutableAttributedString *realMutable = [[NSMutableAttributedString alloc] init];
id mutable = [OCMockObject niceMockForClass:[realMutable class]];
id string = [OCMockObject niceMockForClass:[NSAttributedString class]];
[[[mutable expect] andReturn:#"YO" ] initWithAttributedString:string];
NSLog(#"MOCK: %#", [mutable initWithAttributedString:string]);
[mutable verify];
// Outputs 'MOCK: YO' and passes

NSArray exception when adding track to starred playlist

Sometimes, I'm getting a fatal exception when trying to "star" a track in cocoalibspotify. I'm logging in with a user with a heavy dataset (hundreds of playlists, but < 15 starred tracks).
This is how I "star" the SPTrack:
[[[[SPSession sharedSession] starredPlaylist] items] addObject:myTrack];
... and the stack trace:
*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[NSArray objectsAtIndexes:]: index 12 beyond bounds [0 .. 11]'
*** Call stack at first throw:
(
0 CoreFoundation 0x01e125a9 __exceptionPreprocess + 185
1 libobjc.A.dylib 0x021a1313 objc_exception_throw + 44
2 CoreFoundation 0x01da0f99 -[NSArray objectsAtIndexes:] + 633
3 Foundation 0x016a250b -[NSKeyValueArray objectsAtIndexes:] + 110
4 Foundation 0x016aaca6 NSKeyValueDidChangeByArrayMutation + 103
5 Foundation 0x01610c30 NSKeyValueDidChange + 266
6 Foundation 0x016aba95 -[NSObject(NSKeyValueObserverNotification) didChange:valuesAtIndexes:forKey:] + 123
7 Foundation 0x016a4d0e -[NSKeyValueNotifyingMutableArray addObject:] + 239
8 MyApp 0x000922cd -[PlaylistManager starTrack:] + 285
...
This only seems to happen within a minute or two after logging in (i.e. user data is loaded), so I'm guessing it might be an issue where the data isn't fully loaded or something?
I've tried to find out if there's any properties to observe in order to find out when everything's fully loaded. But as the array might be empty (and the user might not have starred any tracks before), it seems like there's no good way of validating that everything's loaded...? The loaded property of SPPlaylist seems to refer to the playlist metadata, and not its tracks (?).
Any ideas?
You are getting the So it looks like you're not supposed to manipulate that array directly. Instead call [myTrack setStarred:YES];
Basically, I just went through the entire Spotify API for you to find that answer.
A full code sample for this:
-(void)starTrack:(NSString *)uri {
NSURL *trackURL = [NSURL URLWithString:uri];
[[SPSession sharedSession] trackForURL:trackURL callback:^(SPTrack *track) {
if (track != nil) {
[SPAsyncLoading waitUntilLoaded:track timeout:kSPAsyncLoadingDefaultTimeout then:^(NSArray *tracks, NSArray *notLoadedTracks) {
[track setStarred:YES];
}];
}
}];
}

Resources