Search code examples
objective-ccore-datansmanagedobjectcontext

Core data - Save with an array causing 'Illegal attempt to establish a relationship'


In core data, I have a relationship which is joined via a bridge entity

The relationship in question is Card ->> Segment ->> Fighter

  • A card has many segments
  • A segment has many fighters A single
  • A fighter can appear on one segment

One segment (or SegmentRow) has an array of fighters attached to it.

A diagram of it can be seen below;

Entity diagram

The way the app works currently is, a user picks a fighter to add to a segment row. Once they hit save, the segment rows and fighters assigned to that row are saved.

But I keep getting a relationship error;

Illegal attempt to establish a relationship 'fighters' between objects in different contexts (source = Segment -- Name: Row 0 with 0 fighters , destination = Some fighter)'

Some notes:

The card object doesn't exist yet, this is because the segments need to be saved first; then the card object can be saved.

So my code to save is like this;

+ (void) saveSegments:(NSArray *)segments inContext:(NSManagedObjectContext *)context  withCompletion:(void (^)(BOOL success, NSError *error))completion
{
    int i = 0;
    for (FCSegmentRow *row in segments) {

        FCSegment *segment = [FCSegment MR_createEntityInContext:context];
        segment.name = [NSString stringWithFormat:@"Row %lu", (unsigned long)i];


        // Fighters
        NSMutableSet *fighters = [NSMutableSet set];

        for (FCFighter *fighter in row.fighters) {
            if (fighter) {
                [fighters addObject:fighter];
            }
        }

        segment.fighters = [NSSet setWithSet:fighters];


        i++;
    }
    [context MR_saveToPersistentStoreAndWait];


    // Output
    NSArray *allSegments = [self segmentsInContext:context error:nil];
    for (FCSegment *segment in allSegments) {
        NSLog(@"%@", [segment description]);
    }
}

The FCSegmentRow is just a simple NSObject with an NSArray *fighters;

In my code, I try to set a mutable set and add fighters to this set, then save it to core data.

Further, I tried to put the saving in a block and wait for completion; but this issue continues to happen.

It appears to be because the fighter is in a different context to the segment I am creating.

So, the question I have is -- how do I store the segment with its fighters (relationship)

Edit: Where and how are fighters created?

With reference to questions

  1. The fighters are on their own page (view controller) and has its own managed object context.

  2. The page which manages the segments allows a user to pick from a list of fighters; this launches the fighters page and waits for a completion block to "return" the selected fighter.

  3. The fighters data comes from Firebase, and loads it into Core data.

The fighters view controller is configured thusly;

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.title = @"Pick Fighter";

    self.tableView.delegate = self;
    self.tableView.dataSource = self;
    self.context = [NSManagedObjectContext MR_context];

    [self reloadData];
}

// ... 

-(void) reloadData
{
        self.fighterList = [FCFighter findFightersInContext:self.context];

    [self.tableView reloadData];
}

As seen above, the fighters view controller has its own context

Edit: Further notes:

A card consists of many segments, I call this a segment row.

On the "segment row view controller" there is 2 buttons per table cell (A segment has 2 fighters), the user can press either button and it fires the fighters page, letting the user select the required fighter.

I tried to pass the same context as is in the segment row view controller into the fighters page, but what happens is that it loses the data from the first button as soon as I press the second button.

IE: Press Button A - Pick a fighter .. Joe Bloggs Press Button B - Pick a fighter .. Mike Smith When I return to the segment row; Press Button A's fighter is disappeared; and when I check the data its (null).

The code to fire up the fighters page is below;

UIStoryboard *mainStoryboard = [UIStoryboard mainStoryboard];
    FCFightersTableViewController *vc = (FCFightersTableViewController *) [mainStoryboard instantiateViewControllerWithIdentifier:@"FCFightersTableViewController"];
    vc.tableCellSelectable = YES;
    vc.excludeFighters = [selectedFightersMutable copy];
    //vc.context = self.context;
    vc.completionBlock = ^(FCFightersTableViewController *vc, FCFighter *fighter) {

        NSLog(@"Picked fighter - %@ (uuid: %@)", fighter.name, fighter.uuid);

        [fightersInRow setObject:fighter atIndexedSubscript:fighterIdx];
        segmentRow.fighters = [fightersInRow copy];


        NSLog(@"selected fighter - %@ (uuid: %@)", fighter.name, fighter.uuid);

        [fightersInRow setObject:fighter atIndexedSubscript:fighterIdx];
        segmentRow.fighters = [fightersInRow copy];

        [self.tableView beginUpdates];
        [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
        [self.tableView endUpdates];
    };

Solution

  • I am accepting @pbasdf's answer.

    Here's what I did. I didn't use a global context, rather I kept it so that each view controller has its own NSManagedObjectContext.

    When a user is on the SegmentRow (Page A) a user can press a button which launches a list of fighters (Page B); a completion block is returned;

    // (Page A) fires Page B
    
        UIStoryboard *mainStoryboard = [UIStoryboard mainStoryboard];
            FCFightersTableViewController *vc = (FCFightersTableViewController *) [mainStoryboard instantiateViewControllerWithIdentifier:@"FCFightersTableViewController"];
    
            vc.completionBlock = ^(FCFightersTableViewController *vc, NSManagedObjectID *fighterObjectID) {
    
                FCFighter *fighter = [self.context objectWithID:fighterObjectID];
                NSLog(@"Picked fighter - %@ (uuid: %@)", fighter.name, fighter.uuid);
    
                [fightersInRow replaceObjectAtIndex:fighterIdx withObject:fighter];
                segmentRow.fighters = [fightersInRow copy];
    
                [self.tableView beginUpdates];
                [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
                [self.tableView endUpdates];
            };
    

    Notice I changed it from setObject to replaceObject as per the above comments.

    I also get the ManagedObjectID and use the context to get the fighter, as per pbasdf's answer.

    // (Page B) fighters page fires the completionBlock
    -(void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
    {
     [tableView deselectRowAtIndexPath:indexPath animated:NO];
    
        FCFighter *fighter = nil;
            fighter = [self.fighterList objectAtIndex:indexPath.row];
    
        if (self.completionBlock)
        {
            self.completionBlock(self, fighter.objectID);
        }
    
        [self.navigationController dismissViewControllerAnimated:YES completion:^{
            [MBProgressHUD hideHUDForView:self.view animated:YES];
        }];
    }
    

    When I come to save it, I now;

    // Save the segments to Core data
        for (FCSegmentRow *row in segments) {
    
            FCSegment *segment = [FCSegment MR_createEntityInContext:context];
            segment.name = [NSString stringWithFormat:@"Row %lu", (unsigned long)i];
    
            [segment addFighters:[NSSet setWithArray:row.fighters]];
        }
        [context MR_saveToPersistentStoreAndWait];
    
    
        // Output log
        NSArray *allSegments = [self segmentsInContext:context error:nil];
        for (FCSegment *segment in allSegments) {
            NSLog(@"%@", [segment description]);
            for (FCFighter *fighter in [segment.fighters allObjects]) {
                NSLog(@"Fighter -- %@", fighter.name);
            }
        }
    

    Although I get a core data error I will put this in another question, when I hit the output log it displays the fighters for the given segment.

    I have to do some checks to see if its persisting and fetching the objects correctly; but other than this I am accepting @pbasdf's answer.

    Many thanks