Relationship of NSManagedObject to an NSArrayController - objective-c

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;
}
}

Related

Core Data MOC change not triggered when editing certain values

In my app I have two NSOutlineView tables. My represented objects have dynamic attributes, so I have to add columns dynamically.
This is what the objects look like:
Instance properties
I use Core Data and Cocoa bindings to display and edit the values.
I can display all the attributes in the outline view, and (seemingly) edit them, too, but only certain properties trigger an NSManagedObjectContextObjectsDidChangeNotification. That means, that the data is not saved for certain columns -- unless I modify a value in a correctly working column, too. In that case, all data is subsequently saved, even the ones that did not trigger the context change.
This is a correctly working column:
NSTableColumn *nameColumn = [[NSTableColumn alloc] initWithIdentifier:#"Name"];
nameColumn.title = #"Name";
nameColumn.width = 200;
[self.instancesOutlineView addTableColumn:nameColumn];
[nameColumn bind:NSValueBinding
toObject:self.instancesTreeController
withKeyPath:#"arrangedObjects.name"
options:nil];
However, the columns which are created from the NSDictionaryproperty do not trigger the context change:
NSTreeNode *selectedNode = [self.typesOutlineView itemAtRow:[self.typesOutlineView selectedRow]];
InstanceType *it = (InstanceType *)[selectedNode representedObject];
// Get Instance Dynamic Attributes dictionary
Instance *i = it.instances.anyObject;
NSDictionary *instanceDynAtts = i.dynamicAttributes;
for (NSString *key in instanceDynAtts) {
NSTableColumn *column = [[NSTableColumn alloc] initWithIdentifier:key];
column.title = key;
[self.instancesOutlineView addTableColumn:column];
NSString *keyPath = [NSString stringWithFormat:#"arrangedObjects.dynamicAttributes.%#.value", key];
[column bind:NSValueBinding
toObject:self.instancesTreeController
withKeyPath:keyPath
options:nil];
}
What could be the problem here? Is there a way to force saving the context, even if it hasn't changed?

How to name Undo menu entries for Core Data add/remove items via bindings and NSArrayController?

I have a NSTableView populated by a Core Data entity and Add Item / Remove Item buttons all wired with a NSArrayController and bindings in Interface Builder.
The Undo/Redo menu items can undo or redo the add / remove item actions.
But the menu entries are called only „Undo“ resp. „Redo“.
How can i name them like „Undo Add Item“, „Undo Remove Item“, etc.
(I am aware, something similar was asked before, but the accepted answers are either a single, now rotten link or the advice to subclass NSManagedObject and override a method that Apples documentation says about: "Important: You must not override this method.“)
Add a subclass of NSArrayController as a file in your project. In the xib, in the Identity Inspector of the array controller, change the Class from NSArrayController to your new subclass.
Override the - newObject method.
- (id)newObject
{
id newObj = [super newObject];
NSUndoManager *undoManager = [[[NSApp delegate] window] undoManager];
[undoManager setActionName:#"Add Item"];
return newObj;
}
Also the - remove:sender method.
- (void)remove:(id)sender
{
[super remove:sender];
NSUndoManager *undoManager = [[[NSApp delegate] window] undoManager];
[undoManager setActionName:#"Remove Item"];
}
Register for NSManagedObjectContextObjectsDidChangeNotification:
[[NSNotificationCenter defaultCenter] addObserver: self
selector: #selector(mocDidChangeNotification:)
name:NSManagedObjectContextObjectsDidChangeNotification
object: nil];
And parse the userInfo dictionary in the corresponding method:
- (void)mocDidChangeNotification:(NSNotification *)notification
{
NSManagedObjectContext* savedContext = [notification object];
// Ignore change notifications for anything but the mainQueue MOC
if (savedContext != self.managedObjectContext) {
return;
}
// Ignore updates -- lots of noise from maintaining user-irrelevant data
// Set actionName for insertion
for (NSManagedObject* insertedObject in
[notification.userInfo valueForKeyPath:NSInsertedObjectsKey])
{
NSString* objectClass = NSStringFromClass([insertedObject class]);
savedContext.undoManager.actionName = savedContext.undoManager.isUndoing ?
[NSString stringWithFormat:#"Delete %#", objectClass] :
[NSString stringWithFormat:#"Insert %#", objectClass];
}
// Set actionName for deletion
for (NSManagedObject* deletedObject in
[notification.userInfo valueForKeyPath:NSDeletedObjectsKey])
{
NSString* objectClass = NSStringFromClass([deletedObject class]);
savedContext.undoManager.actionName = savedContext.undoManager.isUndoing ?
[NSString stringWithFormat:#"Insert %#", objectClass] :
[NSString stringWithFormat:#"Delete %#", objectClass];
}
}
I've tested this in my own code-- it's rough. Can spend a lot more time making the actionName nicer. I deleted parsing of updates because: 1) insertions and deletions of objects in to-many relationships generate updates of other objects 2) I don't care to figure out how to discover what properties changed at this time
I also have class names that aren't user-friendly, so this is a great time to implement the description function for all entities, and use that rather than the class name.
But this at least works for all object controllers in a project, and easily enough for insert and delete.
[edit] Updated with mikeD's suggestion to cover redo having an inverse name. Thanks!

Pass data from sheet to insertNewObject in a parent/child relationship

I have a problem to insertNewObject in an entity being the child in a parent/child relationship. It's a CoreData app with a local SQLite base. There are 2 entities presented with 2 TableViews on the main window. Using contentSet, the table for the child will only show data relating to the selected parent.
Adding data to the child is done by showing a sheet with a table of items coming from a 3rd entity. User must pick from this table then click Add. On dismissing the sheet, the child table on main window should be updated with a new row. Problem: nothing appears.
Checking the database content with a third-party app, I see that the new data is there but it doesn't appear on the table view because no info on the relationship with parent was stored, so it doesn't know to which parent it relates.
My code is missing info about this but I just don't see how I should program this. In other words: on dismissing sheet, identify which parent is selected and specify this relationship info when inserting the new data in the child. I would appreciate any help.
Here is my code:
// there are 3 entities: Collectors (parent), CollectedItems (child) and Items.
// we call the sheet presenting the Items list to pick from
- (IBAction)showAddItemDialog:(id)sender {
[NSApp beginSheet:addItemDialog
modalForWindow:window
modalDelegate:self
didEndSelector:#selector(didEndAddItemSheet:returnCode:contextInfo:)
contextInfo:nil];
}
// we dismiss the sheet by clicking on Cancel or Add
- (IBAction)dismissAddItemDialog:(id)sender {
[NSApp endSheet:addItemDialog returnCode:([sender tag])];
[addItemDialog orderOut:sender];
}
// depending on clicked button, do nothing or pass selected data
- (void)didEndAddItemSheet:(NSWindow *)sheet returnCode:(int)returnCode contextInfo (void *)contextInfo {
if (returnCode == 0) {
// do nothing, this is the effect of clicking on Cancel
}
if (returnCode == 1) {
NSString *theName = [[[itemsPickerController selectedObjects] valueForKey:#"itemName"] objectAtIndex:0];
// above, we get the value from the itemName attribute sleected in the Items list
NSLog (#"%#", theName);
// the variable is displayed in the console, so it was correctly selected
[self addNewItemWithName:theName];
}
}
// we use the passed data to create new object (row) in the CollectedItems entity
- (id)addNewItemWithName:(NSString *)theName {
NSEntityDescription *newContent = [NSEntityDescription insertNewObjectForEntityForName:#"CollectedItems" inManagedObjectContext:[self managedObjectContext]];
[newContent setValue:theName forKey:#"collectedItemName"];
// above, we insert a new row in CollectedItems, assigning the value theName to the attribute collectedItemName
return nil;
}
You need to add the relationship between your CollectedItems object you created and its parent the Collectors object. Collectors will have a core data utility method on it (if you have generated the core data managed object classes). It will be called something like addCollectedItemsObject.
Reloading the tableViews should then update with the correct data - as it now knows the relationship.
Even better would be to use NSFetchedResultsController to control the data in your tables so that when you update your data model, the tables will automatically reflect the changes.
And for the record, for those it might help, here is the final working code:
Ensuring the method was generated in the corresponding Collectors class file for (core data managed object classes).
// header file
#class Collecteditems;
#interface Collectors : NSManagedObject { }
#property (nonatomic, retain) NSSet* content;
#end
#interface Collectors (CoreDataGeneratedAccessors)
- (void)addContentObject:(Collecteditems *)value;
#end
// implementation file
#import "Collectors.h"
#import "Collecteditems.h"
#implementation Collectors
#dynamic content;
- (void)addContentObject:(Collecteditems *)value {
NSSet *s = [NSSet setWithObject:value];
[self willChangeValueForKey:#"collecteditems" withSetMutation:NSKeyValueUnionSetMutation usingObjects:s];
[[self primitiveValueForKey:#"collecteditems"] addObject:value];
[self didChangeValueForKey:#"collecteditems" withSetMutation:NSKeyValueMinusSetMutation usingObjects:s];
}
Then adding the addObject method on the NSArrayController controlling the collected items table (see id in original post for reference).
- (id)addNewItemWithName:(NSString *)theName {
NSEntityDescription *newContent = [NSEntityDescription insertNewObjectForEntityForName:#"CollectedItems" inManagedObjectContext:[self managedObjectContext]];
[newContent setValue:theName forKey:#"collectedItemName"];
[collectedItemsController addObject:newContent]; // line added
return nil;
}

Getting NSFetchedResultsController, NSSortDescription and sectionNameForKeyPath to work together

I'm currently working on an App that has a couple of Entities and relationships as illustrated below:
Item <<--> Category.
I am currently fetching Item instances and displaying them in sections using the item's category.name. In this case I can use a sort descriptor to sort the categories by name, which is pretty straightforward and working fine (relevant code below):
-(NSFetchedResultsController*)fetchedResultsController {
if (fetchedResultsController_ != nil)
return fetchedResultsController_;
NSManagedObjectContext *moc = [order_ managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Item" inManagedObjectContext:moc];
[fetchRequest setEntity:entity];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"category.name" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
[sortDescriptors release];
[sortDescriptor release];
NSFetchedResultsController *controller = [[NSFetchedResultsController alloc]
initWithFetchRequest:fetchRequest
managedObjectContext:moc
sectionNameKeyPath:#"category.name"
cacheName:nil];
controller.delegate = self;
self.fetchedResultsController = controller;
[controller release];
[fetchRequest release];
NSError *error = nil;
if (![fetchedResultsController_ performFetch:&error]) {
// Error handling
}
return fetchedResultsController_;
}
My problem now is that I need to sort these categories not by name, but by a (NSNumber*) displayOrder attribute that is part of the Category entity. BUT I need the section titles on the tableview to continue to use the categorie's name.
If I set the sortDescriptor to use category.displayOrder and keep the sectionNameKeyPath as category.name, the section titles work fine but the sortDescriptor is simply ignored by the fetchedResultsController and the table sections are ordered by the category's name (not sure why??).
My next idea was to overwrite the displayOrder getter method but that didn't get me too far as the return types are different, plus I needed the actual displayOrder value for the section sorting.
So right now I have a solution which feels a bit clunky (code below), and I am wondering if there is a better way of achieving the same thing using the fetchedResultsController alone.
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
// The code below grabs a reference to first object for a given section
// and uses it to return the associated category name
{
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
NSArray *menuItems = [sectionInfo objects];
if ([menuItems count] > 0)
{
MenuItem *menuItem = [menuItems objectAtIndex:0];
NSString *categoryName = menuItem.category.name;
return categoryName;
}
return [sectionInfo name];
}
Am I missing something basic here?
Thanks in advance for your thoughts.
Rog
That's a perfectly good solution to the problem, Rog.
You certainly don't want/need to subclass NSFetchedResultsController.
#aroth, we don't have enough information to know the details of his object model, but the names certainly imply that category does not own item. Item has a category. His intent is to display a list of items, that is why he is fetching items.
As far as sorting goes, the documentation has this to say:
If the controller generates sections, the first sort descriptor in the array is used to group the objects into sections; its key must either be the same as sectionNameKeyPath or the relative ordering using its key must match that using sectionNameKeyPath.
In English (you may already know this Rog, but then again you may not and certainly people who search this later may appreciate the explanation), that means if you're using sections then the sorting on the NSFetchRequest must group all items in the same section together. This could be by making the first sort criteria be the field used as the section name, or it could be by making the first sort criteria be something else that results in the same sort of grouping.
The documentation doesn't specify what happens if you screw this up; it's possible it would just totally screw up the section names, repeat sections, skip sections, detect the situation and "fix" your sorting, or even just crash. Do any of your categories have the same displayOrder?
Your solution is certainly workable, and if you can't get it to work correctly sorting by displayOrder while titling sections by category.name it's probably your best solution.
Why are you fetching Item and not Category? If I understand your relationships correctly, Category owns a 1 to many relationship with Item, so in theory a Category instance should have an 'items' property that returns every Item in that category.
If that's the case, then you could simply fetch all of your categories, and then sort them by displayOrder. Then, forget about using sections in the NSFetchedResultsController itself. Instead, your associated tableView methods would look something like:
- (NSInteger)numberOfSections {
return [[self.fetchedResultsController fetchedObjects] count];
}
- (NSInteger)numberOfRowsInSection:(NSInteger)section {
Category* category = [[self.fetchedResultsController fetchedObjects] objectAtIndex:section];
return [[category items] count];
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
Category* category = [[self.fetchedResultsController fetchedObjects] objectAtIndex:section];
return category.name;
}
In short, I think you are overcomplicating things by fetching Item instead of Category, and by trying to make the NSFetchedResultsController manage your section grouping for you. It is much simpler and requires much less code to just do the section management yourself.
You probably need to subclass NSFetchedResultsController and customize the section name functions. See the NSFetchedResultsController class docs on subclassing.

NSTokenField representing Core Data to-many relationship

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?