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 中的重复的主要内容,如果未能解决你的问题,请参考以下文章