Я пытаюсь найти образец кода, который показывает, как обрабатывать перемещение / перестановку ячеек в tableView, когда ячейка использует fetchedResultsController (т.е. в сочетании с Core Data). Я получаю вызов moveRowAtIndexPath: к моему источнику данных, но не могу найти правильную комбинацию черной магии, чтобы таблица / данные правильно распознали изменение.
Например, когда я перемещаю строку 0 в строку 2, а затем отпускаю ее, она "выглядит" правильно. Затем нажимаю "Готово". Строка (1), которая сдвинулась вверх, чтобы заполнить строку 0, по-прежнему имеет вид в режиме редактирования (значки минус и перемещение), в то время как другие строки ниже возвращаются к обычному виду. Если я затем прокручиваю вниз, поскольку строка 2 (изначально 0, помните?) Приближается к верху, она полностью исчезает.
Какого черта. Нужно ли мне каким-то образом аннулировать fetchedResultsController? Всякий раз, когда я устанавливаю его на ноль, я получаю вылеты. Должен ли я выпустить его вместо этого? Я в сорняках?
Вот что у меня там сейчас ...
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
NSManagedObjectContext *context = [fetchedResultsController managedObjectContext];
/*
Update the links data in response to the move.
Update the display order indexes within the range of the move.
*/
if (fromIndexPath.section == toIndexPath.section) {
NSInteger start = fromIndexPath.row;
NSInteger end = toIndexPath.row;
NSInteger i = 0;
if (toIndexPath.row < start)
start = toIndexPath.row;
if (fromIndexPath.row > end)
end = fromIndexPath.row;
for (i = start; i <= end; i++) {
NSIndexPath *tempPath = [NSIndexPath indexPathForRow:i inSection:toIndexPath.section];
LinkObj *link = [fetchedResultsController objectAtIndexPath:tempPath];
//[managedObjectContext deleteObject:[fetchedResultsController objectAtIndexPath:tempPath]];
link.order = [NSNumber numberWithInteger:i];
[managedObjectContext refreshObject:link mergeChanges:YES];
//[managedObjectContext insertObject:link];
}
}
// Save the context.
NSError *error;
if (![context save:&error]) {
// Handle the error...
}
}
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller is about to start sending change notifications, so prepare the table view for updates.
if (self.theTableView != nil)
[self.theTableView beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller has sent all current change notifications, so tell the table view to process all updates.
if (self.theTableView != nil) {
[self.theTableView endUpdates];
}
}
Я решил это немного по-другому (опубликую, когда он будет протестирован). Для этого действительно нужен Reverse-NSFetchedResultsController. По сути, подайте NSFetchedResultsController UITableView, порядок сортировки и заголовки разделов, и пусть он все записывает обратно в CoreData. Думаю, я мог бы попытаться собрать это для развлечения. Corey Floyd 15.09.2009
Абсолютно ... что-то более элегантное, чем мое решение, которое обеспечит двунаправленную синхронизацию между сохраненными данными и пользовательским интерфейсом таблицы. Я хотел бы увидеть, что вы придумали, когда закончите. Greg Combs 21.09.2009
Как бы то ни было, Мэтт Лонг недавно опубликовал хороший учебник по переупорядочению строк в таблице, поддерживаемой NSFetchedResultsController: cimgf.com/2010/06/05/re-ordering-nsfetchedresultscontroller Clint Harris 22.07.2010
См. Это простое решение: stackoverflow.com/a/15625897/308315 iwasrobbed 26.03.2013
Обычно, когда вы видите подобные артефакты, пользовательский интерфейс был анимирован в новое положение и сообщил вам об этом, тогда обновления, которые вы сделали для своей модели, неправильно отражают состояние, которое приводит к сбоям в следующий раз, когда view должен ссылаться на модель для обновления.
Я думаю, вы не совсем понимаете, что вы должны делать в этом методе. Он вызывается, потому что пользовательский интерфейс изменился, и он должен позволить модели измениться соответствующим образом. В приведенном ниже коде предполагается, что результаты уже находятся в новом порядке, и вам просто нужно по какой-то причине сбросить поле заказа:
Загвоздка в том, что вы фактически не меняете порядок в базовой модели. Эти indexPaths взяты из UITableViewController, он сообщает вам, что пользователь перетаскивал их между ними, и вам необходимо обновить соответствующие данные. Но fetchedResultsController всегда находится в порядке сортировки, поэтому, пока вы не измените эти свойства, ничего не перемещается.
Дело в том, что они не были перемещены, вас вызывают, чтобы сказать вам, что вам нужно переместить их (путем настройки свойства sortable). Вам действительно нужно что-то вроде:
Это заставит объекты переупорядочить, а затем пройти и очистить любой из порядковых номеров других объектов, которые должны были сдвинуться вверх, зная, что индексы могли переместиться.
Ответ 2 · рейтинг 8 · Greg Combs
Вот то, что официально работает сейчас, с удалениями, перемещениями и вставками. Я "подтверждаю" заказ каждый раз, когда на него влияет действие редактирования.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.section != kHeaderSection) {
if (editingStyle == UITableViewCellEditingStyleDelete) {
@try {
LinkObj * link = [self.fetchedResultsController objectAtIndexPath:indexPath];
debug_NSLog(@"Deleting at indexPath %@", [indexPath description]);
//debug_NSLog(@"Deleting object %@", [link description]);
if ([self numberOfBodyLinks] > 1)
[self.managedObjectContext deleteObject:link];
}
@catch (NSException * e) {
debug_NSLog(@"Failure in commitEditingStyle, name=%@ reason=%@", e.name, e.reason);
}
}
else if (editingStyle == UITableViewCellEditingStyleInsert) {
// we need this for when they click the "+" icon; just select the row
[theTableView.delegate tableView:tableView didSelectRowAtIndexPath:indexPath];
}
}
}
- (BOOL)validateLinkOrders {
NSUInteger index = 0;
@try {
NSArray * fetchedObjects = [self.fetchedResultsController fetchedObjects];
if (fetchedObjects == nil)
return NO;
LinkObj * link = nil;
for (link in fetchedObjects) {
if (link.section.intValue == kBodySection) {
if (link.order.intValue != index) {
debug_NSLog(@"Info: Order out of sync, order=%@ expected=%d", link.order, index);
link.order = [NSNumber numberWithInt:index];
}
index++;
}
}
}
@catch (NSException * e) {
debug_NSLog(@"Failure in validateLinkOrders, name=%@ reason=%@", e.name, e.reason);
}
return (index > 0 ? YES : NO);
}
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
NSArray * fetchedObjects = [self.fetchedResultsController fetchedObjects];
if (fetchedObjects == nil)
return;
NSUInteger fromRow = fromIndexPath.row + NUM_HEADER_SECTION_ROWS;
NSUInteger toRow = toIndexPath.row + NUM_HEADER_SECTION_ROWS;
NSInteger start = fromRow;
NSInteger end = toRow;
NSInteger i = 0;
LinkObj *link = nil;
if (toRow < start)
start = toRow;
if (fromRow > end)
end = fromRow;
@try {
for (i = start; i <= end; i++) {
link = [fetchedObjects objectAtIndex:i]; //
//debug_NSLog(@"Before: %@", link);
if (i == fromRow) // it's our initial cell, just set it to our final destination
link.order = [NSNumber numberWithInt:(toRow-NUM_HEADER_SECTION_ROWS)];
else if (fromRow < toRow)
link.order = [NSNumber numberWithInt:(i-1-NUM_HEADER_SECTION_ROWS)]; // it moved forward, shift back
else // if (fromIndexPath.row > toIndexPath.row)
link.order = [NSNumber numberWithInt:(i+1-NUM_HEADER_SECTION_ROWS)]; // it moved backward, shift forward
//debug_NSLog(@"After: %@", link);
}
}
@catch (NSException * e) {
debug_NSLog(@"Failure in moveRowAtIndexPath, name=%@ reason=%@", e.name, e.reason);
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
@try {
switch (type) {
case NSFetchedResultsChangeInsert:
[theTableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
[self validateLinkOrders];
break;
case NSFetchedResultsChangeUpdate:
break;
case NSFetchedResultsChangeMove:
self.moving = YES;
[self validateLinkOrders];
break;
case NSFetchedResultsChangeDelete:
[theTableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self validateLinkOrders];
break;
default:
break;
}
}
@catch (NSException * e) {
debug_NSLog(@"Failure in didChangeObject, name=%@ reason=%@", e.name, e.reason);
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
switch(type) {
case NSFetchedResultsChangeInsert:
[self.theTableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.theTableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller has sent all current change notifications, so tell the table view to process all updates.
@try {
if (self.theTableView != nil) {
//[self.theTableView endUpdates];
if (self.moving) {
self.moving = NO;
[self.theTableView reloadData];
//[self performSelector:@selector(reloadData) withObject:nil afterDelay:0.02];
}
[self performSelector:@selector(save) withObject:nil afterDelay:0.02];
}
}
@catch (NSException * e) {
debug_NSLog(@"Failure in controllerDidChangeContent, name=%@ reason=%@", e.name, e.reason);
}
}
Ответ 3 · рейтинг 7 · JosephH
На самом деле лучший ответ - в комментарии Клинта Харриса по этому вопросу:
Чтобы быстро подвести итог, необходимо иметь свойство displayOrder для объектов, которые вы пытаетесь изменить, с описанием сортировки для упорядочивания полученного контроллера результатов в этом поле. Тогда код для moveRowAtIndexPath:toIndexPath: выглядит так:
- (void)tableView:(UITableView *)tableView
moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath
toIndexPath:(NSIndexPath *)destinationIndexPath;
{
NSMutableArray *things = [[fetchedResultsController fetchedObjects] mutableCopy];
// Grab the item we're moving.
NSManagedObject *thing = [[self fetchedResultsController] objectAtIndexPath:sourceIndexPath];
// Remove the object we're moving from the array.
[things removeObject:thing];
// Now re-insert it at the destination.
[things insertObject:thing atIndex:[destinationIndexPath row]];
// All of the objects are now in their correct order. Update each
// object's displayOrder field by iterating through the array.
int i = 0;
for (NSManagedObject *mo in things)
{
[mo setValue:[NSNumber numberWithInt:i++] forKey:@"displayOrder"];
}
[things release], things = nil;
[managedObjectContext save:nil];
}
Документация Apple также содержит важные подсказки:
В общем, NSFetchedResultsController разработан, чтобы реагировать на изменения на уровне модели. Если вы разрешаете пользователю переупорядочивать строки таблицы, то ваша реализация методов делегата должна учитывать это.
Обычно, если вы разрешаете пользователю переупорядочивать строки таблицы, ваш объект модели имеет атрибут, определяющий его индекс. Когда пользователь перемещает строку, вы соответствующим образом обновляете этот атрибут. Это, однако, имеет побочный эффект, заставляя контроллер заметить изменение и, таким образом, сообщить своему делегату об обновлении (используя controller: didChangeObject: atIndexPath: forChangeType: newIndexPath :). Если вы просто используете реализацию этого метода, показанную в разделе "Типичное использование", тогда делегат попытается обновить табличное представление. Однако табличное представление уже находится в соответствующем состоянии из-за действий пользователя.
В общем, поэтому, если вы поддерживаете обновления, управляемые пользователем, вы должны установить флаг, если перемещение инициировано пользователем. В реализации методов делегата, если установлен флаг, вы обходите реализации основных методов; Например:
Когда вы перемещаете строку в представлении таблицы, вы фактически перемещаете блок других строк (состоящий хотя бы из одной строки) в другом направлении одновременно. Хитрость заключается в том, чтобы обновить только свойство displayOrder этого блока и перемещенного элемента.
Во-первых, убедитесь, что свойство displayOrder всех строк установлено в соответствии с текущим порядком отображения таблиц. Здесь нам не нужно сохранять контекст, мы сохраним его позже, когда фактическая операция перемещения завершится:
- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
[super setEditing:editing animated:animated];
[_tableView setEditing:editing animated:animated];
if(editing) {
NSInteger rowsInSection = [self tableView:_tableView numberOfRowsInSection:0];
// Update the position of all items
for (NSInteger i=0; i<rowsInSection; i++) {
NSIndexPath *curIndexPath = [NSIndexPath indexPathForRow:i inSection:0];
SomeManagedObject *curObj = [_fetchedResultsController objectAtIndexPath:curIndexPath];
NSNumber *newPosition = [NSNumber numberWithInteger:i];
if (![curObj.displayOrder isEqualToNumber:newPosition]) {
curObj.displayOrder = newPosition;
}
}
}
}
Тогда единственное, что вам нужно сделать, это обновить положение перемещенного элемента и всех элементов между fromIndexPath и toIndexPath:
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
NSInteger moveDirection = 1;
NSIndexPath *lowerIndexPath = toIndexPath;
NSIndexPath *higherIndexPath = fromIndexPath;
if (fromIndexPath.row < toIndexPath.row) {
// Move items one position upwards
moveDirection = -1;
lowerIndexPath = fromIndexPath;
higherIndexPath = toIndexPath;
}
// Move all items between fromIndexPath and toIndexPath upwards or downwards by one position
for (NSInteger i=lowerIndexPath.row; i<=higherIndexPath.row; i++) {
NSIndexPath *curIndexPath = [NSIndexPath indexPathForRow:i inSection:fromIndexPath.section];
SomeManagedObject *curObj = [_fetchedResultsController objectAtIndexPath:curIndexPath];
NSNumber *newPosition = [NSNumber numberWithInteger:i+moveDirection];
curObj.displayOrder = newPosition;
}
SomeManagedObject *movedObj = [_fetchedResultsController objectAtIndexPath:fromIndexPath];
movedObj.displayOrder = [NSNumber numberWithInteger:toIndexPath.row];
NSError *error;
if (![_fetchedResultsController.managedObjectContext save:&error]) {
NSLog(@"Could not save context: %@", error);
}
}
Стол не может перезагружаться во время движения, перезагрузите его после задержки, и все будет в порядке.
Ответ 6 · рейтинг 1 · Jorge Ortiz
Извини, Грег, я уверен, что делаю что-то не так, но твой ответ мне не подходит.
Хотя все мои объекты проходят проверку правильно, когда я выхожу из режима редактирования, одна из строк зависает (элементы управления редактированием не исчезают), и впоследствии ячейки не реагируют должным образом.
Возможно, моя проблема в том, что я не знаю, как использовать заданное вами свойство moving (self.moving = YES). Не могли бы вы прояснить это? Большое Вам спасибо.
[‹> IsEditing] может использоваться, чтобы определить, разрешено ли редактирование таблицы или нет. Вместо того, чтобы откладывать это, как предлагается, используя следующий оператор