Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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!

like image 527
simonwh Avatar asked Oct 07 '10 11:10

simonwh


1 Answers

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.

like image 196
Era Avatar answered Nov 15 '22 11:11

Era