NSTokenField representing Core Data to-many relationship - objective-c

I'm having a problem figuring out how to represent a many-to-many relationship model in a NSTokenField. I have two (relevant) models:
Item
Tag
An item can have many tags and a tag can have many items. So it's an inverse to-many relationship.
What I would like to do is represent these tags in a NSTokenField. I would like to end up with a tokenfield automatically suggesting matches (found out a way to do that with tokenfield:completionsForSubstring:indexOfToken:indexOfSelectedItem) and being able to add new tag entities if it wasn't matched to an existing one.
Okay, hope you're still with me. I'm trying to do all this with bindings and array controllers (since that makes most sense, right?)
I have an array controller, "Item Array Controller", that is bound to my app delegates managedObjectContext. A tableview showing all items has a binding to this array controller.
My NSTokenField's value has a binding to the array controllers selection key and the model key path: tags.
With this config, the NSTokenField won't show the tags. It just gives me:
<NSTokenFieldCell: 0x10014dc60>: Unknown object type assigned (Relationship objects for {(
<NSManagedObject: 0x10059bdc0> (entity: Tag; id: 0x10016d6e0 <x-coredata://9D77D47A-1171-4397-9777-706F599D7E3B/Tag/p102> ; data: <fault>)
)} on 0x100169660). Ignoring...
This makes sense to me, so no worries. I've looked at some of the NSTokenField delegate methods and it seems that I should use:
- (NSString *)tokenField:(NSTokenField *)tokenField displayStringForRepresentedObject:(id)representedObject
Problem is, this method is not called and I get the same error as before.
Alright, so my next move was to try and make a ValueTransformer. Transforming from an array with tag entity -> array with strings (tag names) was all good. The other way is more challenging.
What I've tried is to look up every name in my shared app delegate managed object context and return the matching tags. This gives me a problem with different managed object contexts apparently:
Illegal attempt to establish a relationship 'tags' between objects in different contexts (source = <NSManagedObject: 0x100156900> (entity: Item; id: 0x1003b22b0 <x-coredata://9D77D47A-1171-4397-9777-706F599D7E3B/Item/p106> ; data: {
author = "0x1003b1b30 <x-coredata://9D77D47A-1171-4397-9777-706F599D7E3B/Author/p103>";
createdAt = nil;
filePath = nil;
tags = (
);
title = "Great presentation";
type = "0x1003b1150 <x-coredata://9D77D47A-1171-4397-9777-706F599D7E3B/Type/p104>";
}) , destination = <NSManagedObject: 0x114d08100> (entity: Tag; id: 0x100146b40 <x-coredata://9D77D47A-1171-4397-9777-706F599D7E3B/Tag/p102> ; data: <fault>))
Where am I going wrong? How do I resolve this? Is it even the right approach (seems weird to me that you woud have to use a ValueTransformer?)
Thanks in advance!

I've written a custom NSValueTransformer to map between the bound NSManagedObject/Tag NSSet and the NSString NSArray of the token field. Here are the 2 methods:
- (id)transformedValue:(id)value {
if ([value isKindOfClass:[NSSet class]]) {
NSSet *set = (NSSet *)value;
NSMutableArray *ary = [NSMutableArray arrayWithCapacity:[set count]];
for (Tag *tag in [set allObjects]) {
[ary addObject:tag.name];
}
return ary;
}
return nil;
}
- (id)reverseTransformedValue:(id)value {
if ([value isKindOfClass:[NSArray class]]) {
NSArray *ary = (NSArray *)value;
// Check each NSString in the array representing a Tag name if a corresponding
// tag managed object already exists
NSMutableSet *tagSet = [NSMutableSet setWithCapacity:[ary count]];
for (NSString *tagName in ary) {
NSManagedObjectContext *context = [[NSApp delegate] managedObjectContext];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSPredicate *searchFilter = [NSPredicate predicateWithFormat:#"name = %#", tagName];
NSEntityDescription *entity = [NSEntityDescription entityForName:[Tag className] inManagedObjectContext:context];
[request setEntity:entity];
[request setPredicate:searchFilter];
NSError *error = nil;
NSArray *results = [context executeFetchRequest:request error:&error];
if ([results count] > 0) {
[tagSet addObjectsFromArray:results];
}
else {
Tag *tag = [[Tag alloc] initWithEntity:entity insertIntoManagedObjectContext:context];
tag.name = tagName;
[tagSet addObject:tag];
[tag release];
}
}
return tagSet;
}
return nil;
}
CoreData seems to automatically establish the object relationships on return (but I have not completely verified this yet)
Hope it helps.

Your second error is caused by having two separate managed object context with the same model and store active at the same time. You are trying to create an object in one context and then relate it another object in the second context. That is not allowed. You need to lose the second context and make all your relationships within a single context.
Your initial error is caused by an incomplete keypath. From your description it sounds like you are trying to populate the token fields with ItemsArrayController.selectedItem.tags but that will just return a Tag object which the token filed cannot use. Instead, you need to provide it with something that converts to a string e.g. ItemsArrayController.selectedItem.tags.name

2 questions:
1) Do you have an NSManagedObjectContext being used other than your app delegate's context?
2) Is the object that implements tokenField:displayStringForRepresentedObject: set as the delegate for the NSTokenField?

Related

valueForKeyPath failing with nested NSMutableDictionary objects

On my class I have two data objects: 'dataDict' (property) and 'prevDict' (instance variable) which are both NSMutableDictionary objects. In these multi-level dictionaries all the 'nodes' are themselves NSMutableDictionary objects and all the 'leaves' are NSString values.
Most of the time, these dictionaries will compare to be the same. But, when the new incoming data is different, I want to capture those changes via KVO and then save the new incoming dictionary. The recursive calls are the thing that fires off all the KVO changes.
Below is the code for recursively determining if the new incoming data (in property 'dataDict') is different than what's in the 'prevDictionary'. The initial call is:
[self postChangesFromPrevDict:prevDict usingKeyPath:#"dataDict"];
prevDict = [[NSMutableDictionary alloc] initWithDictionary:self.dataDict copyItems:YES];
If there's a change on a given leaf, it should update that in the dictionary and produce a KVO notification.
A sample 'newPath' string value could be "dataDict.group.name.points" -- in which the 'key' value is "points".
In the debugger breakpoint noted, I can see the values of 'curStr' and 'newStr' as, say, "120" and "121" with the these two values being correctly obtained from prevDict and dataDict, respectively.
If I obtain a value from a given keyPath then set it again using that SAME key path, why am I getting an NSUnknownKeyException?
I do know that all my dictionaries involved are mutable.
-(void) postChangesFromPrevDict:(NSMutableDictionary *)prevDictionary
usingKeyPath:(NSString *)keyPath
{
NSArray *keys = [prevDictionary allKeys];
for (NSString *key in keys) {
id obj = [prevDictionary objectForKey:key];
NSString *newPath = [keyPath stringByAppendingString:[NSString stringWithFormat:#".%#",key]];
if ([obj isKindOfClass:[NSDictionary class]])
[self postChangesFromPrevDict:obj usingKeyPath:newPath];
else {
NSString *curStr = obj;
NSString *newStr = [self valueForKeyPath:newPath];
//debug breakpoint
if (![newStr isEqualToString:curStr]) {
#try {
[self setValue:newStr forKeyPath:newPath];
}
#catch (NSException *__unused exception) {
NSLog(#"Can't set key on %#",newPath);
}
}
}
}
}
PS: To recurse, I test for an NSDictionary class which is fine as NSMutableDictionary is a subclass thereof.

Core data find-or-create most efficient way

I have around 10000 objects of entity 'Message'. When I add a new 'Message' i want to first see whether it exists - and if it does just update it's data, but if it doesn't to create it.
Right now the "find-or-create" algorithm works with by saving all of the Message objects 'objectID' in one array and then filtering through them and getting the messages with existingObjectWithID:error:
This works fine but in my case when I fetch an 'Message' using existingObjectWithID: and then try to set and save a property by setting the property of the 'Message' object and calling save: on it's context it doesn't saves it properly. Has anyone come across a problem like this?
Is there a more efficient way to make find-or-create algorithm?
First, Message is a "bad" name for a CoreData entity as apple use it internally and it cause problems later in development.
You can read a little more about it HERE
I've noticed that all suggested solutions here use an array or a fetch request.
You might want to consider a dictionary based solution ...
In a single threaded/context application this is accomplished without too much of a burden by adding to cache (dictionary) the newly inserted objects (of type Message) and pre-populating the cache with existing object ids and keys mapping.
Consider this interface:
#interface UniquenessEnforcer : NSObject
#property (readonly,nonatomic,strong) NSPersistentStoreCoordinator* coordinator;
#property (readonly,nonatomic,strong) NSEntityDescription* entity;
#property (readonly,nonatomic,strong) NSString* keyProperty;
#property (nonatomic,readonly,strong) NSError* error;
- (instancetype) initWithEntity:(NSEntityDescription *)entity
keyProperty:(NSString*)keyProperty
coordinator:(NSPersistentStoreCoordinator*)coordinator;
- (NSArray*) existingObjectIDsForKeys:(NSArray*)keys;
- (void) unregisterKeys:(NSArray*)keys;
- (void) registerObjects:(NSArray*)objects;//objects must have permanent objectIDs
- (NSArray*) findOrCreate:(NSArray*)keys
context:(NSManagedObjectContext*)context
error:(NSError* __autoreleasing*)error;
#end
flow:
1) on application start, allocate a "uniqueness enforcer" and populate your cache:
//private method of uniqueness enforcer
- (void) populateCache
{
NSManagedObjectContext* context = [[NSManagedObjectContext alloc] init];
context.persistentStoreCoordinator = self.coordinator;
NSFetchRequest* r = [NSFetchRequest fetchRequestWithEntityName:self.entity.name];
[r setResultType:NSDictionaryResultType];
NSExpressionDescription* objectIdDesc = [NSExpressionDescription new];
objectIdDesc.name = #"objectID";
objectIdDesc.expression = [NSExpression expressionForEvaluatedObject];
objectIdDesc.expressionResultType = NSObjectIDAttributeType;
r.propertiesToFetch = #[self.keyProperty,objectIdDesc];
NSError* error = nil;
NSArray* results = [context executeFetchRequest:r error:&error];
self.error = error;
if (results) {
for (NSDictionary* dict in results) {
_cache[dict[self.keyProperty]] = dict[#"objectID"];
}
} else {
_cache = nil;
}
}
2) when you need to test existence simply use:
- (NSArray*) existingObjectIDsForKeys:(NSArray *)keys
{
return [_cache objectsForKeys:keys notFoundMarker:[NSNull null]];
}
3) when you like to actually get objects and create missing ones:
- (NSArray*) findOrCreate:(NSArray*)keys
context:(NSManagedObjectContext*)context
error:(NSError* __autoreleasing*)error
{
NSMutableArray* fullList = [[NSMutableArray alloc] initWithCapacity:[keys count]];
NSMutableArray* needFetch = [[NSMutableArray alloc] initWithCapacity:[keys count]];
NSManagedObject* object = nil;
for (id<NSCopying> key in keys) {
NSManagedObjectID* oID = _cache[key];
if (oID) {
object = [context objectWithID:oID];
if ([object isFault]) {
[needFetch addObject:oID];
}
} else {
object = [NSEntityDescription insertNewObjectForEntityForName:self.entity.name
inManagedObjectContext:context];
[object setValue:key forKey:self.keyProperty];
}
[fullList addObject:object];
}
if ([needFetch count]) {
NSFetchRequest* r = [NSFetchRequest fetchRequestWithEntityName:self.entity.name];
r.predicate = [NSPredicate predicateWithFormat:#"SELF IN %#",needFetch];
if([context executeFetchRequest:r error:error] == nil) {//load the missing faults from store
fullList = nil;
}
}
return fullList;
}
In this implementation you need to keep track of objects deletion/creation yourself.
You can use the register/unregister methods (trivial implementation) for this after a successful save.
You could make this a bit more automatic by hooking into the context "save" notification and updating the cache with relevant changes.
The multi-threaded case is much more complex (same interface but different implementation altogether when taking performance into account).
For instance, you must make your enforcer save new items (to the store) before returning them to the requesting context as they don't have permanent IDs otherwise, and even if you call "obtain permanent IDs" the requesting context might not save eventually.
you will also need to use a dispatch queue of some sort (parallel or serial) to access your cache dictionary.
Some math:
Given:
10K (10*1024) unique key objects
average key length of 256[byte]
objectID length of 128[byte]
we are looking at:
10K*(256+128) =~ 4[MB] of memory
This might be a high estimate, but you should take this into account ...
Ok, many things can go wrong here this is how to:
Create NSManagedObjectContext -> MOC
Create NSFetchRequest with the right entity
Create the NSPredicate and attache it to the fetch request
execute fetch request on newly created context
fetch request will return an array of objects matching the predicate
(you should have only one object in that array if your ids are distinct)
cast first element of an array to NSManagedObject
change its property
save context
The most important thing of all is that you use the same context for fetching and saving, and u must do it in the same thread cause MOC is not thread safe and that is the most common error that people do
Currently you say you maintain an array of `objectID's. When you need to you:
filter through them and get the messages with existingObjectWithID:error:
and after this you need to check if the message you got back:
exists
matches the one you want
This is very inefficient. It is inefficient because you are always fetching objects back from the data store into memory. You are also doing it individually (not batching). This is basically the slowest way you could possibly do it.
Why changes to that object aren't saved properly isn't clear. You should get an error of some kind. But, you should really change your search approach:
Instead of looping and loading, use a single fetch request with a predicate:
NSFetchRequest *request = ...;
NSPredicate *filterPredicate = [NSPredicate predicateWithFormat:#"XXX == %#", YYY];
[request setPredicate:filterPredicate];
[request setFetchLimit:1];
where XXX is the name of the attribute in the message to test, and YYY is the value to test it against.
When you execute this fetch on the MOC you should get one or zero responses. If you get zero, create + insert a new message and save the MOC. If you get one, update it and save the MOC.

Core data debugger hangs at assigning value to entity

I have a core data entity to which I am trying to assign relationship from another entity. Please refer the code below
#define kId #"id"
-(NSArray *)fetchObjectsForEntityName:(NSString *)entityName withPredicate:(NSPredicate *)predicate
{
NSManagedObjectContext *newContext = [Helper generateNewContext];
NSEntityDescription *entity = [NSEntityDescription entityForName:entityName inManagedObjectContext:newContext];
NSFetchRequest *request = [NSFetchRequest new];
[request setEntity:entity];
if (predicate)
[request setPredicate:predicate];
NSError *error = nil;
NSArray *resultArray = [newContext executeFetchRequest:request error:&error];
return resultArray;
}
-(void)updateCoreDataEntity
{
NSArray *objectsArray = [self fetchObjectsForEntityName:#"FirstEntity" withPredicate:nil];
//FirstObjects is a subclass of NSManagedObject class (Custom entity)
//kId is just #define as defined above
//Recasting removed
for (FirstObjects *firstObject in objectsArray) {
if ([firstObject.id isEqualToString:[dict valueForKey:kId]]) {
secondEntity.firstEntity = firstObject; //debugger hangs here
}
}
}
I am trying to fetch objects that belong to "FirstEntity" into an NSArray
Loop through that array to find the required object.
Then assign the "firstObject" to SecondEntity if the criteria matches.
However, I am getting nowhere with this code as the debugger (and the code) hangs at the last line of code.
What is the mistake I am doing, can anyone help with this code.
Regards,
iSee
secondEntity is maybe undefined. This would surely lead to a crash.
Also, the logic of the ID is flawed. It seems the comparison is not comparing to a specific ID but to the generic string "id". Perhaps you really want to compare to a dynamically allocated id? Also, are these string ids unique? (If not you might get unpredictable results.)
Finally, from the code it is not clear if Helper provides always the same managed object context. This would be strongly advised - separate contexts are mainly used for concurrency.

Relationship of NSManagedObject to an NSArrayController

What I've got:
controller1 an NSArrayController
controller2 an NSArrayController. This has a parent relationship to an attribute of controller1.
controller1-2Tree an NSTreeController and an NSOutlineView to view it. This shows the hierarchy of controller1 and the children of each item that it found from the parent/child relationship of controller2. This has been done by binding the two NSArrayControllers to the values and children of the tree.
The Problem:
Everything in my situation uses core-bindings. Yet, unlike an NSTableView, the unorthodox set up of my NSOutlineView means that my current selection isn't passed onto my relevant NSArrayController. For example, if I select a child in my controller1-2Tree, it's an object from my controller2, but controller2 itself doesn't register the selection change.
I have the relevant code to pick up on selection changes. I'm unsure of how to manually change the current selection item of controller2 or controller1 (although it's 2 that I need right now), based on knowing the current selected item of controller1-2Tree.
I've worked out how to isolate the currently selected object, I'm just missing the last step on how to relate this to the NSArrayController without iterating through it based on trying to match a property.
NSManagedObject *selectedObject = [[controller1-2View itemAtRow:[controller1-2View selectedRow]] representedObject];
NSManagedObjectContext *selectedObjectContext = [selectedObject managedObjectContext];
Ok, I went the hard way that I was trying to avoid and got this to work by iterating through objects and controllers. I'm sure there's a better way than this.
if ([controller1-2view parentForItem:[controller1-2view itemAtRow:[controller1-2view selectedRow]]]) {
// If not nil; then the item has a parent. If nil, it doesn't and isn't selectable.
NSManagedObject *selectedProject = [[controller1-2view itemAtRow:[controller1-2view selectedRow]] representedObject];
NSString *selectedProjectName = [selectedProject valueForKey:#"title"];
NSFetchRequest *controller2FetchRequest = [[NSFetchRequest alloc] init];
NSManagedObjectContext *moc= [controller2 managedObjectContext];
NSEntityDescription *controller2Entity = [NSEntityDescription entityForName:#"entityTitle" inManagedObjectContext:moc];
[controller2FetchRequest setEntity:entityTitle];
NSError *controller2FetchError = nil;
newArray = [moc executeFetchRequest:controller2FetchRequest error:&controller2FetchError];
NSInteger projectCounter = 0;
[controller2FetchRequest release];
for (NSString *s in newArray) {
NSManagedObject *projectMo = [newArray objectAtIndex:projectCounter]; // assuming that array is not empty
id projectValue = [projectMo valueForKey:#"title"];
//NSLog(#"projectValue is %# and selectedProjectName is %#", projectValue, selectedProjectName);
if (projectValue == selectedProjectName) {
//NSLog(#"Match found");
[controller2 setSelectionIndex:projectCounter];
NSLog(#"Selected in arrayController: %#", [controller2Controller selectedObjects]);
}
projectCounter = projectCounter + 1;
}
}

Getting an object from an NSSet

If you can't get an object with objectAtIndex: from an NSSet then how do you retrieve objects?
There are several use cases for a set. You could enumerate through (e.g. with enumerateObjectsUsingBlock or NSFastEnumeration), call containsObject to test for membership, use anyObject to get a member (not random), or convert it to an array (in no particular order) with allObjects.
A set is appropriate when you don't want duplicates, don't care about order, and want fast membership testing.
NSSet doesn't have a method objectAtIndex:
Try calling allObjects which returns an NSArray of all the objects.
it is possible to use filteredSetUsingPredicate if you have some kind of unique identifier to select the object you need.
First create the predicate (assuming your unique id in the object is called "identifier" and it is an NSString):
NSPredicate *myPredicate = [NSPredicate predicateWithFormat:#"identifier == %#", identifier];
And then choose the object using the predicate:
NSObject *myChosenObject = [mySet filteredSetUsingPredicate:myPredicate].anyObject;
NSArray *myArray = [myNSSet allObjects];
MyObject *object = [myArray objectAtIndex:(NSUInteger *)]
replace NSUInteger with the index of your desired object.
For Swift3 & iOS10 :
//your current set
let mySet : NSSet
//targetted index
let index : Int
//get object in set at index
let object = mySet.allObjects[index]
NSSet uses the method isEqual: (which the objects you put into that set must override, in addition, the hash method) to determine if an object is inside of it.
So, for example if you have a data model that defines its uniqueness by an id value (say the property is:
#property NSUInteger objectID;
then you'd implement isEqual: as
- (BOOL)isEqual:(id)object
{
return (self.objectID == [object objectID]);
}
and you could implement hash:
- (NSUInteger)hash
{
return self.objectID; // to be honest, I just do what Apple tells me to here
// because I've forgotten how Sets are implemented under the hood
}
Then, you can get an object with that ID (as well as check for whether it's in the NSSet) with:
MyObject *testObject = [[MyObject alloc] init];
testObject.objectID = 5; // for example.
// I presume your object has more properties which you don't need to set here
// because it's objectID that defines uniqueness (see isEqual: above)
MyObject *existingObject = [mySet member: testObject];
// now you've either got it or existingObject is nil
But yeah, the only way to get something out of a NSSet is by considering that which defines its uniqueness in the first place.
I haven't tested what's faster, but I avoid using enumeration because that might be linear whereas using the member: method would be much faster. That's one of the reasons to prefer the use of NSSet instead of NSArray.
for (id currentElement in mySet)
{
// ** some actions with currentElement
}
Most of the time you don't care about getting one particular object from a set. You care about testing to see if a set contains an object. That's what sets are good for. When you want to see if an object is in a collection sets are much faster than arrays.
If you don't care about which object you get, use -anyObject which just gives you one object from the set, like putting your hand in a bag and grabbing something.
Dog *aDog = [dogs anyObject]; // dogs is an NSSet of Dog objects
If you care about what object you get, use -member which gives you back the object, or nil if it's not in the set. You need to already have the object before you call it.
Dog *spot = [Dog dogWithName:#"Spot"];
// ...
Dog *aDog = [dogs member:spot]; // Returns the same object as above
Here's some code you can run in Xcode to understand more
NSString *one = #"One";
NSString *two = #"Two";
NSString *three = #"Three";
NSSet *set = [NSSet setWithObjects:one, two, three, nil];
// Can't use Objective-C literals to create a set.
// Incompatible pointer types initializing 'NSSet *' with an expression of type 'NSArray *'
// NSSet *set = #[one, two, three];
NSLog(#"Set: %#", set);
// Prints looking just like an array but is actually not in any order
//Set: {(
// One,
// Two,
// Three
// )}
// Get a random object
NSString *random = [set anyObject];
NSLog(#"Random: %#", random); // Random: One
// Iterate through objects. Again, although it prints in order, the order is a lie
for (NSString *aString in set) {
NSLog(#"A String: %#", aString);
}
// Get an array from the set
NSArray *array = [set allObjects];
NSLog(#"Array: %#", array);
// Check for an object
if ([set containsObject:two]) {
NSLog(#"Set contains two");
}
// Check whether a set contains an object and return that object if it does (nil if not)
NSString *aTwo = [set member:two];
if (aTwo) {
NSLog(#"Set contains: %#", aTwo);
}