Изменения фоновой очереди в родительском NSManagedObjectContext в UIManagedDocument приводят к дублированию в NSFetchedresultsController при слиянии - PullRequest
2 голосов
/ 19 марта 2012

Хорошо, ребята. Этот доводит меня до стены. У меня

  • UIManagedDocument и его 2 MOContexts (обычный и родительский.)
  • UITableViewController (подкласс к CoreDataTableViewController Пола Хегарти), который запускается из
  • NSFetchedResultsController
  • Фоновая очередь GCD для синхронизации с сервером, к которому обращается родительский вызов

Я пробовал так много разных способов, и каждый раз сталкиваюсь с проблемами.

Когда я добавляю новую "животную" сущность, это не проблема и сразу же появляется на столе. Но когда я загружаю его на сервер (в очереди выгрузки) и изменяю его «статус» (с родительским контекстом), чтобы он был в загруженном разделе, он появляется там, но не исчезает из раздела без загрузки .

Я заканчиваю с близнецами, которых я не хочу! или иногда он даже не делает правильный, а просто оставляет неправильный.

*** НО, дополнительный исчезнет, ​​когда приложение закроется и перезагрузится. Так что это просто где-то в памяти. Я могу проверить в магазине, что все правильно. Но 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 Ответ

3 голосов
/ 12 апреля 2012

Слишком много кода для того, чтобы кто-нибудь захотел пройти через него.

Однако, после быстрой проверки, похоже, что вы нарушаете ограничения MOC.В частности, вы обращаетесь к родительскому контексту напрямую, а не из его собственного потока.

Как правило, вы начинаете новый поток, затем создаете MOC в этом потоке, и его родитель становится MOC длядокумент.затем сделайте свое дело и позвоните сохранить на новом MOC.Затем он уведомит родителя, который должен обработать обновление.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...