NSTokenField representing Core Data to-many relationship
Asked Answered
R

3

6

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!

Recuperative answered 7/10, 2010 at 11:0 Comment(1)
I spent some more time researching this today - still not able to find resources explaining this. I hope someone will come to the rescue here! :)Recuperative
M
7

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.

Marston answered 19/4, 2011 at 11:47 Comment(0)
R
1

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

Rudnick answered 19/4, 2011 at 14:12 Comment(0)
K
0

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?

Kirst answered 24/2, 2011 at 13:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.