UIManagedDocument 中对父 NSManagedObjectContext 的后台队列更改导致合并时 NSFetchedresultsController 中的重复

Posted

技术标签:

【中文标题】UIManagedDocument 中对父 NSManagedObjectContext 的后台队列更改导致合并时 NSFetchedresultsController 中的重复【英文标题】:Background-queue changes to parent NSManagedObjectContext in UIManagedDocument cause duplicate in NSFetchedresultsController on merge 【发布时间】:2012-03-19 04:30:43 【问题描述】:

好的,伙计们。这个把我逼到了墙角。我有

UIManagedDocument 及其 2 个 MOContext(常规和父级)。 一个 UITableViewController(由 Paul Hegarty 归类为 CoreDataTableViewController),运行于 NSFetchedResultsController 后台 GCD 队列,用于与父 cue 访问的服务器同步

我尝试了很多不同的方法,但每次都遇到问题。

当我添加一个新的“动物”实体时,它没有问题并立即出现在桌子上。但是,当我将其上传到服务器(在上传队列上)并更改其“状态”(使用父上下文)以使其应位于已上传部分时,它会出现在那里但不会从未上传部分中消失.

我最终得到了我不想要的双胞胎!或者它有时甚至不会做出正确的,而只是保留错误的。

***但是,当应用程序关闭并重新加载时,多余的会消失。所以它只是在某个地方的内存中。我可以在商店里验证一切都是正确的。但是 NSFetchedResultsController 没有触发 controllerDidChange... 的东西。

这是我的视图控制器的超类

CoreDataTableViewController.m
#pragma mark - Fetching

- (void)performFetch

self.debug = 1;
if (self.fetchedResultsController) 
    if (self.fetchedResultsController.fetchRequest.predicate) 
        if (self.debug) NSLog(@"[%@ %@] fetching %@ with predicate: %@", NSStringFromClass([self class]), NSStringFromSelector(_cmd), self.fetchedResultsController.fetchRequest.entityName, self.fetchedResultsController.fetchRequest.predicate);
     else 
        if (self.debug) NSLog(@"[%@ %@] fetching all %@ (i.e., no predicate)", NSStringFromClass([self class]), NSStringFromSelector(_cmd), self.fetchedResultsController.fetchRequest.entityName);
    
    NSError *error;
    [self.fetchedResultsController performFetch:&error];
    if (error) NSLog(@"[%@ %@] %@ (%@)", NSStringFromClass([self class]), NSStringFromSelector(_cmd), [error localizedDescription], [error localizedFailureReason]);
 else 
    if (self.debug) NSLog(@"[%@ %@] no NSFetchedResultsController (yet?)", NSStringFromClass([self class]), NSStringFromSelector(_cmd));

[self.tableView reloadData];


- (void)setFetchedResultsController:(NSFetchedResultsController *)newfrc

NSFetchedResultsController *oldfrc = _fetchedResultsController;
if (newfrc != oldfrc) 
    _fetchedResultsController = newfrc;
    newfrc.delegate = self;
    if ((!self.title || [self.title isEqualToString:oldfrc.fetchRequest.entity.name]) && (!self.navigationController || !self.navigationItem.title)) 
        self.title = newfrc.fetchRequest.entity.name;
    
    if (newfrc) 
        if (self.debug) NSLog(@"[%@ %@] %@", NSStringFromClass([self class]), NSStringFromSelector(_cmd), oldfrc ? @"updated" : @"set");
        [self performFetch]; 
     else 
        if (self.debug) NSLog(@"[%@ %@] reset to nil", NSStringFromClass([self class]), NSStringFromSelector(_cmd));
        [self.tableView reloadData];
    



#pragma mark - UITableViewDataSource

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView

if (self.debug) NSLog(@"fetchedResultsController returns %d sections", [[self.fetchedResultsController sections] count]);
return [[self.fetchedResultsController sections] count];


- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section

return [[[self.fetchedResultsController sections] objectAtIndex:section] numberOfObjects];


- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section

    return [[[self.fetchedResultsController sections] objectAtIndex:section] name];


- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:  (NSInteger)index

return [self.fetchedResultsController sectionForSectionIndexTitle:title atIndex:index];


    - (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView

return [self.fetchedResultsController sectionIndexTitles];


#pragma mark - NSFetchedResultsControllerDelegate

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller

if (!self.suspendAutomaticTrackingOfChangesInManagedObjectContext) 

    [self.tableView beginUpdates];
    self.beganUpdates = YES;



- (void)controller:(NSFetchedResultsController *)controller
  didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
       atIndex:(NSUInteger)sectionIndex
 forChangeType:(NSFetchedResultsChangeType)type

    if (!self.suspendAutomaticTrackingOfChangesInManagedObjectContext)

    switch(type)
    
        case NSFetchedResultsChangeInsert:
            [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;
    




- (void)controller:(NSFetchedResultsController *)controller
   didChangeObject:(id)anObject
   atIndexPath:(NSIndexPath *)indexPath
 forChangeType:(NSFetchedResultsChangeType)type
  newIndexPath:(NSIndexPath *)newIndexPath
       
if(self.debug) NSLog(@"controller didChangeObject: %@", anObject);
if (!self.suspendAutomaticTrackingOfChangesInManagedObjectContext)

NSLog(@"#########Controller did change type: %d", type);    
switch(type)
    
        case NSFetchedResultsChangeInsert:
            [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeUpdate:
            [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeMove:
            [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
            [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;
    

   

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller

if (self.beganUpdates) [self.tableView endUpdates];
if (self.debug) NSLog(@"controller Did Change Content");


- (void)endSuspensionOfUpdatesDueToContextChanges

_suspendAutomaticTrackingOfChangesInManagedObjectContext = NO;


- (void)setSuspendAutomaticTrackingOfChangesInManagedObjectContext:(BOOL)suspend

if (suspend) 
    _suspendAutomaticTrackingOfChangesInManagedObjectContext = YES;
 else 
    [self performSelector:@selector(endSuspensionOfUpdatesDueToContextChanges) withObject:0 afterDelay:0];



@end

这是我从它继承而来的特定视图控制器:

- (NSArray *)sectionHeaderTitles

if (_sectionHeaderTitles == nil) _sectionHeaderTitles = [NSArray arrayWithObjects:@"Not Yet Uploaded", @"Uploaded But Not Featured", @"Previously Featured", nil];
return _sectionHeaderTitles;


- (NSDictionary *)selectedEntry

if (_selectedEntry == nil) _selectedEntry = [[NSDictionary alloc] init];
return _selectedEntry;


- (void)setupFetchedResultsController

[self.photoDatabase.managedObjectContext setStalenessInterval:0.0];
[self.photoDatabase.managedObjectContext.parentContext setStalenessInterval:0.0];
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Animal"];
request.sortDescriptors = [NSArray arrayWithObjects:[NSSortDescriptor sortDescriptorWithKey:@"status" ascending:YES], [NSSortDescriptor sortDescriptorWithKey:@"unique" ascending:NO], nil];

self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:self.photoDatabase.managedObjectContext sectionNameKeyPath:@"status" cacheName:nil];
NSError *error;
BOOL success = [self.fetchedResultsController performFetch:&error];
if (!success) NSLog(@"error: %@", error); 
else [self.tableView reloadData];
self.fetchedResultsController.delegate = self;


- (void)useDocument

if (![[NSFileManager defaultManager] fileExistsAtPath:[self.photoDatabase.fileURL path]]) 
    [self.photoDatabase saveToURL:self.photoDatabase.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) 
        [self setupFetchedResultsController];
    ];
 else if (self.photoDatabase.documentState == UIDocumentStateClosed) 
    [self.photoDatabase openWithCompletionHandler:^(BOOL success) 
        [self setupFetchedResultsController];

    ];

 else if (self.photoDatabase.documentState == UIDocumentStateNormal) 
    [self setupFetchedResultsController];



- (void)setPhotoDatabase:(WLManagedDocument *)photoDatabase

if (_photoDatabase != photoDatabase) 
    _photoDatabase = photoDatabase;
    [self useDocument];



- (void)viewDidLoad

[super viewDidLoad];

UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
label.backgroundColor = [UIColor clearColor];
label.font = [UIFont fontWithName:@"AmericanTypewriter" size:20];
label.shadowColor = [UIColor colorWithWhite:0.0 alpha:0.5];
label.textAlignment = UITextAlignmentCenter;
label.textColor = [UIColor whiteColor]; 
self.navigationItem.titleView = label;
label.text = self.navigationItem.title;
[label sizeToFit];


- (void)viewWillAppear:(BOOL)animated

[super viewWillAppear:animated];    

// Get CoreData database made if necessary
if (!self.photoDatabase) 
    NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
    url = [url URLByAppendingPathComponent:@"Default Photo Database"];
    self.photoDatabase = [[WLManagedDocument alloc] initWithFileURL:url];
    NSLog(@"No existing photoDatabase so a new one was created from default photo database file.");


self.tableView.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"DarkWoodBackGround.png"]];



- (void)syncWithServer

// This is done on the syncQ

// Start the activity indicator on the nav bar
dispatch_async(dispatch_get_main_queue(), ^ 
    [self.spinner startAnimating];
    self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:self.spinner];

    [[NSNotificationCenter defaultCenter] addObserver:self 
                                             selector:@selector(managedObjectContextDidSave:) 
                                                 name:NSManagedObjectContextDidSaveNotification 
                                               object:self.photoDatabase.managedObjectContext.parentContext];
);


// Find new animals (status == 0)
NSFetchRequest *newAnimalsRequest = [NSFetchRequest fetchRequestWithEntityName:@"Animal"];
newAnimalsRequest.predicate = [NSPredicate predicateWithFormat:@"status == 0"];
NSError *error;
NSArray *newAnimalsArray = [self.photoDatabase.managedObjectContext.parentContext executeFetchRequest:newAnimalsRequest error:&error];
if ([newAnimalsArray count]) NSLog(@"There are %d animals that need to be uploaded.", [newAnimalsArray count]);
if (error) NSLog(@"fetchError: %@", error);

// Get the existing animals from the server
NSArray *parsedDownloadedAnimalsByPhoto = [self downloadedAllAnimalsFromWeb];

// In the parent context, insert downloaded animals into core data

for (NSDictionary *downloadedPhoto in parsedDownloadedAnimalsByPhoto) 
    [Photo photoWithWebDataInfo:downloadedPhoto inManagedObjectContext:self.photoDatabase.managedObjectContext.parentContext];
    // table will automatically update due to NSFetchedResultsController's observing of the NSMOC


// Upload the new animals if there are any
if ([newAnimalsArray count] > 0) 
    NSLog(@"There are %d animals that need to be uploaded.", [newAnimalsArray count]);
    for (Animal *animal in newAnimalsArray) 

        // uploadAnimal returns a number that lets us know if it was accepted by the server
        NSNumber *unique = [self uploadAnimal:animal];
        if ([unique intValue] != 0) 
            animal.unique = unique;

            // uploadThePhotosOf returns a success BOOL if all 3 uploaded successfully
            if ([self uploadThePhotosOf:animal])
                [self.photoDatabase.managedObjectContext performBlock:^
                    animal.status = [NSNumber numberWithInt:1];
                ];
            
        
    


[self.photoDatabase.managedObjectContext.parentContext save:&error];
if (error) NSLog(@"Saving parent context error: %@", error);
[self performUpdate];


// Turn the activity indicator off and replace the sync button
dispatch_async(dispatch_get_main_queue(), ^ 
    // Save the context
    [self.photoDatabase saveToURL:self.photoDatabase.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:^(BOOL success) 
        if (success) 
         
            NSLog(@"Document was saved");
            [self.photoDatabase.managedObjectContext processPendingChanges];
           else 
            NSLog(@"Document was not saved");
        
    ];

    [self.spinner stopAnimating];
    self.navigationItem.leftBarButtonItem = self.syncButton;
);

// Here it skips to the notification I got from saving the context so I can MERGE them


- (NSNumber *)uploadAnimal:(Animal *)animal

NSURL *uploadURL = [NSURL URLWithString:@"index.php" relativeToURL:self.remoteBaseURL];
NSString *jsonStringFromAnimalMetaDictionary = [animal.metaDictionary JSONRepresentation];
NSLog(@"JSONRepresentation of %@: %@", animal.namestring, jsonStringFromAnimalMetaDictionary);
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:uploadURL];
[request setPostValue:jsonStringFromAnimalMetaDictionary forKey:@"newmeta"];
[request startSynchronous];
NSError *error = [request error];
NSString *response;
if (!error) 
    response = [request responseString];
    NSNumber *animalUnique = [(NSArray *)[response JSONValue]objectAtIndex:0];
    return animalUnique;
 else 
    response = [error description]; 
    NSLog(@"%@ got an error: %@", animal.namestring, response);
    return [NSNumber numberWithInt:0];



- (BOOL)uploadThePhotosOf:(Animal *)animal
   
NSURL *uploadURL = [NSURL URLWithString:@"index.php" relativeToURL:self.remoteBaseURL];

int index = [animal.photos count];
for (Photo *photo in animal.photos) 

    // Name the jpeg file
    NSTimeInterval timeInterval = [NSDate timeIntervalSinceReferenceDate];
    NSString *imageServerPath = [NSString stringWithFormat:@"%lf-Photo.jpeg",timeInterval];

    // Update the imageServerPath
    photo.imageURL = imageServerPath;

    NSData *photoData = [[NSData alloc] initWithData:photo.image];
    NSString *photoMeta = [photo.metaDictionary JSONRepresentation];

    ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:uploadURL];
    [request addPostValue:photoMeta forKey:@"newphoto"];
    [request addData:photoData withFileName:imageServerPath andContentType:@"image/jpeg" forKey:@"filename"];
    [request setUploadProgressDelegate:self.progressView];
    [request startSynchronous];
    NSLog(@"%@ progress: %@", animal.namestring, self.progressView.progress);

    NSString *responseString = [request responseString];
    NSLog(@"uploadThePhotosOf:%@ photo at placement: %d has responseString: %@", animal.namestring, [photo.placement intValue], responseString);
    SBJsonParser *parser= [[SBJsonParser alloc] init];
    NSError *error = nil;
    id jsonObject = [parser objectWithString:responseString error:&error];
    NSNumber *parsedPhotoUploadResponse = [(NSArray *)jsonObject objectAtIndex:0];

    // A proper response is not 0
    if ([parsedPhotoUploadResponse intValue] != 0) 
        photo.imageid = parsedPhotoUploadResponse;
        --index;
     


// If the index spun down to 0 then it was successful
int success = (index == 0) ? 1 : 0;
return success;


- (NSArray *)downloadedAllAnimalsFromWeb

NSURL *downloadURL = [NSURL URLWithString:@"index.php" relativeToURL:self.remoteBaseURL];
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:downloadURL];
[request setPostValue:@"yes" forKey:@"all"];
request.tag = kGetHistoryRequest;
[request startSynchronous];
NSString *responseString = [request responseString];
NSLog(@"downloadedAllAnimalsFromWeb responseString: %@", responseString);

SBJsonParser *parser= [[SBJsonParser alloc] init];
NSError *error = nil;
id jsonObject = [parser objectWithString:responseString error:&error];
NSArray *parsedDownloadedResponseStringArray = [NSArray arrayWithArray:jsonObject];
return parsedDownloadedResponseStringArray;


- (void)performUpdate

NSManagedObjectContext * context = self.photoDatabase.managedObjectContext.parentContext;
NSSet                  * inserts = [context updatedObjects];

if ([inserts count])

    NSError * error = nil;

    NSLog(@"There were inserts");
    if ([context obtainPermanentIDsForObjects:[inserts allObjects]
                                        error:&error] == NO)
    
        NSLog(@"BAM! %@", error);
    


[self.photoDatabase updateChangeCount:UIDocumentChangeDone];


- (void)managedObjectContextDidSave:(NSNotification *)notification


[[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:self.photoDatabase.managedObjectContext.parentContext];

NSLog(@"userInfo from the notification: %@", [notification userInfo]);
    // Main thread context
NSManagedObjectContext *context = self.fetchedResultsController.managedObjectContext;

SEL selector = @selector(mergeChangesFromContextDidSaveNotification:); 
[context performSelectorOnMainThread:selector withObject:notification waitUntilDone:YES];
NSLog(@"ContextDidSaveNotification was sent. MERGED");




#pragma mark - Table view data source


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"EntryCell"];

if (!cell) 
    cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"EntryCell"];


// Configure the cell here...
Animal *animal = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = animal.namestring;
if (([animal.numberofanimals intValue] > 0) && animal.species) 
    cell.detailTextLabel.text = [NSString stringWithFormat:@"%@s", animal.species];
 else 
    cell.detailTextLabel.text = animal.species;

return cell;


- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender


NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
Animal *animal = [self.fetchedResultsController objectAtIndexPath:indexPath];
// be somewhat generic here (slightly advanced usage)
// we'll segue to ANY view controller that has a photographer @property
if ([segue.identifier isEqualToString:@"newAnimal"]) 
    NSLog(@"self.photodatabase");
    [(NewMetaEntryViewController *)[segue.destinationViewController topViewController] setPhotoDatabaseContext:self.photoDatabase.managedObjectContext];
 else if ([segue.destinationViewController respondsToSelector:@selector(setAnimal:)]) 
    // use performSelector:withObject: to send without compiler checking
    // (which is acceptable here because we used introspection to be sure this is okay)
    [segue.destinationViewController performSelector:@selector(setAnimal:) withObject:animal];
    NSLog(@"animal: %@ \r\n indexPath: %@", animal, indexPath);



- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section

return 30;


- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView

return nil;


- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
    
NSLog(@"header for section called for section: %d", section);
NSLog(@"fetchedResultsController sections: %@", self.fetchedResultsController.sections);
CGRect headerRect = CGRectMake(0, 0, tableView.bounds.size.width, 30);
UIView *header = [[UIView alloc] initWithFrame:headerRect];
UILabel *headerTitleLabel = [[UILabel alloc] initWithFrame:CGRectMake(5, 5, tableView.bounds.size.width - 10, 20)];
if ([(Animal *)[[[[self.fetchedResultsController sections] objectAtIndex:section] objects] objectAtIndex:0] status] == [NSNumber numberWithInt:0]) 
    headerTitleLabel.text = [self.sectionHeaderTitles objectAtIndex:0];
 else if ([(Animal *)[[[[self.fetchedResultsController sections] objectAtIndex:section] objects] objectAtIndex:0] status] == [NSNumber numberWithInt:1]) 
    headerTitleLabel.text = [self.sectionHeaderTitles objectAtIndex:1];
 else 
    headerTitleLabel.text = [self.sectionHeaderTitles objectAtIndex:2];

headerTitleLabel.textColor = [UIColor whiteColor];
headerTitleLabel.font = [UIFont fontWithName:@"AmericanTypewriter" size:20]; 
headerTitleLabel.backgroundColor = [UIColor clearColor];
headerTitleLabel.alpha = 0.8;
[header addSubview:headerTitleLabel];
return header;

【问题讨论】:

【参考方案1】:

代码太多,任何人都不想涉足。

但是,通过快速检查,您似乎违反了 MOC 限制。具体来说,您是直接访问父上下文,而不是从它自己的线程访问。

通常,您会启动一个新线程,然后在该线程中创建一个 MOC,使其父级成为文档的 MOC。然后做你的事情,并在新的 MOC 上调用 save 。然后它将通知应该处理更新的父级。

【讨论】:

以上是关于UIManagedDocument 中对父 NSManagedObjectContext 的后台队列更改导致合并时 NSFetchedresultsController 中的重复的主要内容,如果未能解决你的问题,请参考以下文章

如何在SQL中对父记录中的记录进行排序

jquery中对父节点和子节点的利用

无法创建 UIManagedDocument

UIManagedDocument 的迁移问题

UIManagedDocument 和 NSFetchedResultsController

从 UIManagedDocument 到普通堆栈的核心数据迁移