Как я могу поддерживать порядок отображения в UITableView с помощью Core Data?

У меня возникли проблемы с тем, чтобы мои объекты Core Data работали нормально и упорядоченно при использовании UITableView.

Я прошел через несколько руководств и других вопросов здесь, в StackOverflow, но, похоже, нет четкого или элегантного способа сделать это - я действительно надеюсь, что что-то упускаю.

У меня есть один объект Core Data с атрибутом int16 под названием «displayOrder». Я использую NSFetchRequest, отсортированный по «displayOrder», чтобы вернуть данные для моего UITableView. Все, кроме повторного заказа, соблюдается. Вот мой (неэффективный) метод moveRowAtIndePath:

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {         

    NSUInteger fromIndex = fromIndexPath.row;  
    NSUInteger toIndex = toIndexPath.row;

    FFObject *affectedObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:fromIndex];  
    affectedObject.displayOrderValue = toIndex;

    [self FF_fetchResults];


    for (NSUInteger i = 0; i < [self.fetchedResultsController.fetchedObjects count]; i++) {  
        FFObject *otherObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:i];  
        NSLog(@"Updated %@ / %@ from %i to %i", otherObject.name, otherObject.state, otherObject.displayOrderValue, i);  
        otherObject.displayOrderValue = i;  
    }

    [self FF_fetchResults];  
}

Может ли кто-нибудь указать мне на хороший пример кода или увидеть, что я делаю не так? Отображение tableview обновляется нормально, и я вижу в своих сообщениях журнала, что свойство displayOrder обновляется. Это просто не постоянное сохранение и перезагрузка, и что-то кажется очень "неуместным" в этой реализации (кроме бесполезной итерации всех моих FFObjects).

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


person Tony Arnold    schedule 30.10.2009    source источник


Ответы (4)


Я взглянул на ваш код, и это может сработать лучше:

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {         

    NSUInteger fromIndex = fromIndexPath.row;  
    NSUInteger toIndex = toIndexPath.row;

    if (fromIndex == toIndex) {
        return;
    }

    FFObject *affectedObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:fromIndex];  
    affectedObject.displayOrderValue = toIndex;

    NSUInteger start, end;
    int delta;

    if (fromIndex < toIndex) {
        // move was down, need to shift up
        delta = -1;
        start = fromIndex + 1;
        end = toIndex;
    } else { // fromIndex > toIndex
        // move was up, need to shift down
        delta = 1;
        start = toIndex;
        end = fromIndex - 1;
    }

    for (NSUInteger i = start; i <= end; i++) {
        FFObject *otherObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:i];  
        NSLog(@"Updated %@ / %@ from %i to %i", otherObject.name, otherObject.state, otherObject.displayOrderValue, otherObject.displayOrderValue + delta);  
        otherObject.displayOrderValue += delta;
    }

    [self FF_fetchResults];  
}
person gerry3    schedule 30.10.2009
comment
Блестяще! Это сработало отлично. Я очень долго смотрел на исходный код - спасибо за вашу помощь. - person Tony Arnold; 02.11.2009
comment
Это должно быть реализовано параллельно с stackoverflow.com/questions/1077568/ - person Anh; 28.01.2010
comment
Если элементы можно удалить, это не сработает. Убедитесь, что вы обновили поле заказа также при удалении строки. - person Kamchatka; 14.06.2010
comment
есть еще одна ошибка, если fromIndex и toIndex оба = 0 (происходит, если вы начинаете двигаться, но перемещаете его обратно), ваше приложение вылетает (end = 4294967295). Вы должны добавить проверку, одинаковы ли они оба. - person Stephan; 31.01.2012
comment
@StephanZehrer, вы действительно видели, как это произошло? Я удивлен, что в этом случае даже вызывается этот метод и что об этом раньше никто не сообщал. Тем не менее, я добавил чек и досрочный возврат по этому делу. Выглядит нормально? - person gerry3; 31.01.2012
comment
@ gerry3, да, он был в моем приложении на симуляторе, а также на iOS 5 на устройстве iPhone. Я просто запускаю отладчик, чтобы проверить .. :) Может быть, не так, что люди перемещают ноль в ноль (только в этом случае сбой приложения из-за высокого значения) - person Stephan; 31.01.2012
comment
Я наткнулся на этот (отличный) ответ, когда искал то же самое. У меня есть один вопрос по поводу решения: не вызывает ли это ненужных обновлений представления? (потому что каждое обновление displayOrder объекта - это объект, который наблюдает FetchResultsController, причем каждое обновление вызывает вызов FRController..didChangeObject) Или я здесь ошибаюсь? - person wintvelt; 10.11.2015

(Это подразумевается как комментарий к ответу gerry3 выше, но я еще не могу комментировать вопросы и ответы других пользователей.)

Небольшое улучшение для решения gerry3 - очень элегантное. Если не ошибаюсь, строчка

otherObject.displayOrderValue += delta;

будет фактически выполнять арифметические операции с указателем, если displayOrderValue не является примитивным типом. Что может быть не тем, что вам нужно. Вместо этого, чтобы установить значение сущности, я предлагаю:

otherObject.displayOrderValue = [NSNumber numberWithInt:[otherObject.displayOrderValue intValue] + delta];

Это должно правильно обновить свойство вашей сущности и избежать сбоев EXC_BAD_ACCESS.

person octy    schedule 17.02.2010
comment
displayOrder - это атрибут сущности Core Data (таким образом, NSNumber) - я использую mogenerator, который автоматически создает примитивные средства доступа к значениям с суффиксом Value, поэтому в этом случае арифметика указателя прекрасна. Спасибо, что указали на ловушку;) - person Tony Arnold; 18.02.2010

Вот полное решение, как управлять индексированной таблицей с основными данными. Ваш атрибут называется displayOrder, я называю его index. Прежде всего, вам лучше разделить контроллер представления и модель. Для этого я использую контроллер модели, который является интерфейсом между представлением и моделью.

Вам нужно управлять тремя случаями, на которые пользователь может влиять через контроллер представления.

  1. Добавление нового объекта
  2. Удаление существующего объекта
  3. Изменить порядок объектов.

Первые два случая добавления и удаления довольно просты. Delete вызывает процедуру с именем renewObjectIndicesUpwardsFromIndex для обновления индексов после удаленного объекта.

- (void)createObjectWithTitle:(NSString*)title {
    FFObject* object = [FFObject insertIntoContext:self.managedObjectContext];

    object.title = title;
    object.index = [NSNumber numberWithInteger:[self numberTotalObjects]];
    [self saveContext];
}

- (void)deleteObject:(FFObject*)anObject {
  NSInteger objectIndex = [anObject.index integerValue];
  [anObject deleteObject];
  [self renewObjectIndicesUpwardsFromIndex:objectIndex];
  [self saveContext];
}

- (void)renewObjectIndicesUpwardsFromIndex:(NSInteger)fromIndex {
  NSFetchRequest* fetchRequest = [[NSFetchRequest alloc] init];
  [fetchRequest setEntity:[NSEntityDescription entityForName:@"Object" inManagedObjectContext:self.managedObjectContext]];

  NSPredicate* predicate = [NSPredicate predicateWithFormat:@"(index > %d)", fromIndex];
  [fetchRequest setPredicate:predicate];

  NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"index" ascending:YES];
  NSArray *sortDescriptors = [NSArray arrayWithObjects:sortDescriptor, nil];

  [fetchRequest setSortDescriptors:sortDescriptors];

  NSError* fetchError = nil;
  NSArray* objects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&fetchError];

  NSInteger index = fromIndex;
  for (FFObject* object in objects) {
    object.index = [NSNumber numberWithInteger:index];
    index += 1;
  }
  [self saveContext];
}

Прежде чем я перейду к процедурам контроллера для изменения порядка, вот часть контроллера представления. Я использую bool isModifyingOrder, похожий на этот ответ. Обратите внимание, что контроллер представления вызывает две функции в контроллере moveObjectOrderUp и moveObjectOrderDown. В зависимости от того, как вы отображаете объекты в табличном представлении - самые новые сначала или самые новые в последнюю очередь, вы можете переключать их.

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath {

  isModifyingOrder = YES;

  NSUInteger fromIndex = sourceIndexPath.row;
  NSUInteger toIndex = destinationIndexPath.row;

  if (fromIndex == toIndex) {
    return;
  }

  FFObject *affectedObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:fromIndex];

  NSInteger delta;
  if (fromIndex < toIndex) {
    delta = toIndex - fromIndex;
    NSLog(@"Moved down by %lu cells", delta);
    [self.objectController moveObjectOrderUp:affectedObject by:delta];
  } else {
    delta = fromIndex - toIndex;
    NSLog(@"Moved up by %lu cells", delta);
    [self.objectController moveObjectOrderDown:affectedObject by:delta];
  }

  isModifyingOrder = NO;
}

А вот и деталь в контроллере. Это можно было бы написать лучше, но для понимания это, может быть, лучше всего.

- (void)moveObjectOrderUp:(FFObject*)affectedObject by:(NSInteger)delta {
  NSInteger fromIndex = [affectedObject.index integerValue] - delta;
  NSInteger toIndex = [affectedObject.index integerValue];

  if (fromIndex < 1) {
    return;
  }

  NSFetchRequest* fetchRequest = [[NSFetchRequest alloc] init];
  [fetchRequest setEntity:[NSEntityDescription entityForName:@"Object" inManagedObjectContext:self.managedObjectContext]];

  NSPredicate* predicate = [NSPredicate predicateWithFormat:@"(index >= %d) AND (index < %d)", fromIndex, toIndex];
  [fetchRequest setPredicate:predicate];

  NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"index" ascending:YES];
  NSArray *sortDescriptors = [NSArray arrayWithObjects:sortDescriptor, nil];

  [fetchRequest setSortDescriptors:sortDescriptors];

  NSError* fetchError = nil;
  NSArray* objects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&fetchError];

  for (FFObject* object in objects) {
    NSInteger newIndex = [object.index integerValue] + 1;
    object.index = [NSNumber numberWithInteger:newIndex];
  }

  affectedObject.index = [NSNumber numberWithInteger:fromIndex];

  [self saveContext];
}

- (void)moveObjectOrderDown:(FFObject*)affectedObject by:(NSInteger)delta {
  NSInteger fromIndex = [affectedObject.index integerValue];
  NSInteger toIndex = [affectedObject.index integerValue] + delta;

  NSFetchRequest* fetchRequest = [[NSFetchRequest alloc] init];
  [fetchRequest setEntity:[NSEntityDescription entityForName:@"Object" inManagedObjectContext:self.managedObjectContext]];

  NSPredicate* predicate = [NSPredicate predicateWithFormat:@"(index > %d) AND (index <= %d)", fromIndex, toIndex];
  [fetchRequest setPredicate:predicate];

  NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"index" ascending:YES];
  NSArray *sortDescriptors = [NSArray arrayWithObjects:sortDescriptor, nil];

  [fetchRequest setSortDescriptors:sortDescriptors];

  NSError* fetchError = nil;
  NSArray* objects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&fetchError];

  for (FFObject* object in objects)
  {
    NSInteger newIndex = [object.index integerValue] - 1;
    object.index = [NSNumber numberWithInteger:newIndex];
  }

  affectedObject.index = [NSNumber numberWithInteger:toIndex];

  [self saveContext];
}

Не забудьте использовать второй BOOL в вашем контроллере представления для действия удаления, чтобы уведомление о перемещении не выполняло никаких действий. Я назвал это isDeleting и положил сюда.

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
   atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
  newIndexPath:(NSIndexPath *)newIndexPath {
  if (isModifyingOrder) return;

  ...

  switch(type) {

    ...

    case NSFetchedResultsChangeMove:
        if (isDeleting == false) {
            [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:localIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:localNewIndexPath]withRowAnimation:UITableViewRowAnimationFade];
        }
        break;

    ...

  }
}
person Herbert Bay    schedule 07.12.2016

Я так думаю:

    affectedObject.displayOrderValue = toIndex;

должен быть помещен после:

   for (NSUInteger i = start; i <= end; i++) {
    FFObject *otherObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:i];  
    NSLog(@"Updated %@ / %@ from %i to %i", otherObject.name, otherObject.state, otherObject.displayOrderValue, otherObject.displayOrderValue + delta);  
    otherObject.displayOrderValue += delta;
}

и раньше:

    [self FF_fetchResults];  
person Marco Isotti    schedule 30.08.2016