Как реализовать переупорядочение записей CoreData?

Я использую CoreData для своего iPhone-приложения, но CoreData не предоставляет автоматический способ разрешить вам переупорядочивать записи. Я думал использовать другой столбец для хранения информации о заказе, но с помощью непрерывных чисел для индекса упорядочения возникает проблема. если я имею дело с большим количеством данных, переупорядочение записи потенциально включает в себя обновление большого количества записей в информации упорядочения (это похоже на изменение порядка элемента массива)

Какой лучший способ реализовать эффективную схему заказа?

Ответ 1

FetchedResultsController и его делегат не предназначены для использования для изменений модели пользователя. См. справочный документ Apple. Ищите часть, управляемую пользователями. Итак, если вы ищете какой-то магический, однолинейный способ, то не так, к сожалению.

Что вам нужно сделать, так это сделать обновления в этом методе:

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
 userDrivenDataModelChange = YES;

 ...[UPDATE THE MODEL then SAVE CONTEXT]...

 userDrivenDataModelChange = NO;
}

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

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
 if (userDrivenDataModelChange) return;
 ...
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
 if (userDrivenDataModelChange) return;
 ...
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
 if (userDrivenDataModelChange) return;
 ...
}

Я только что реализовал это в своем рабочем приложении (Quickie), и он отлично работает.

Ответ 2

Вот краткий пример, показывающий способ сбросить полученные результаты в NSMutableArray, который вы используете для перемещения ячеек. Затем вы просто обновляете атрибут объекта orderInTable, а затем сохраняете контекст управляемого объекта.

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

Создайте BOOL, который вы можете использовать для временного обхода NSFetchedResultsControllerDelegate

@interface PlaylistViewController ()
{
    BOOL changingPlaylistOrder;
}
@end

Метод делегата представления таблицы:

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath
{
    // Refer to https://developer.apple.com/library/ios/documentation/CoreData/Reference/NSFetchedResultsControllerDelegate_Protocol/Reference/Reference.html#//apple_ref/doc/uid/TP40008228-CH1-SW14

    // Bypass the delegates temporarily
    changingPlaylistOrder = YES;

    // Get a handle to the playlist we're moving
    NSMutableArray *sortedPlaylists = [NSMutableArray arrayWithArray:[self.fetchedResultsController fetchedObjects]];

    // Get a handle to the call we're moving
    Playlist *playlistWeAreMoving = [sortedPlaylists objectAtIndex:sourceIndexPath.row];

    // Remove the call from it current position
    [sortedPlaylists removeObjectAtIndex:sourceIndexPath.row];

    // Insert it at it new position
    [sortedPlaylists insertObject:playlistWeAreMoving atIndex:destinationIndexPath.row];

    // Update the order of them all according to their index in the mutable array
    [sortedPlaylists enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        Playlist *zePlaylist = (Playlist *)obj;
        zePlaylist.orderInTable = [NSNumber numberWithInt:idx];
    }];

    // Save the managed object context
    [commonContext save];

    // Allow the delegates to work now
    changingPlaylistOrder = NO;
}

Теперь ваши делегаты будут выглядеть примерно так:

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

    switch(type)
    {
        case NSFetchedResultsChangeMove:
            [self configureCell:(PlaylistCell *)[self.tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
            break;

    }
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
    if (changingPlaylistOrder) return;

    [self.tableView reloadData];
}

Ответ 3

Поздний ответ: возможно, вы сохранили ключ сортировки в виде строки. Вставка записи между двумя существующими строками может быть выполнена тривиально путем добавления дополнительного символа в строку, например. вставляя "AM" между строками "A" и "B". Переупорядочение не требуется. Аналогичную идею можно выполнить, используя число с плавающей запятой или некоторую простую битовую арифметику для 4-байтового целого числа: вставьте строку со значением ключа сортировки, которая находится на полпути между соседними строками.

Патологические случаи могут возникнуть, когда строка слишком длинная, поплавок слишком мал или больше нет места в int, но тогда вы можете просто перенумеровать объект и начать новый старт. Сканирование и обновление всех ваших записей в редком случае намного лучше, чем повреждение каждого объекта каждый раз, когда пользователь переупорядочивает.

Например, рассмотрим int32. Использование высоких 3 байтов в качестве начального порядка дает вам почти 17 миллионов строк с возможностью вставки до 256 строк между любыми двумя строками. 2 байта позволяют вставить 65000 строк между любыми двумя строками перед повторным сканированием.

Здесь псевдокод, который я имею в виду для 2-байтового приращения и 2 байта для вставки:

AppendRow:item
    item.sortKey = tail.sortKey + 0x10000

InsertRow:item betweenRow:a andNextRow:b
    item.sortKey = a.sortKey + (b.sortKey - a.sortKey) >> 1

Обычно вы вызываете AppendRow, в результате получаем строки с sortKeys из 0x10000, 0x20000, 0x30000 и т.д. Иногда вам придется вставлять InsertRow, скажем между первым и вторым, в результате получается sortKey 0x180000.

Ответ 4

Я применил подход @andrew/@dk с двойными значениями.

Вы можете найти UIOrderedTableView в github.

не стесняйтесь разветвлять его:)

Ответ 5

Я адаптировал это из метода из блога Matt Gallagher (не могу найти оригинальную ссылку). Это может быть не лучшее решение, если у вас есть миллионы записей, но отложите сохранение до тех пор, пока пользователь не закончит переупорядочивать записи.

- (void)moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath sortProperty:(NSString*)sortProperty
{
    NSMutableArray *allFRCObjects = [[self.frc fetchedObjects] mutableCopy];
    // Grab the item we're moving.
    NSManagedObject *sourceObject = [self.frc objectAtIndexPath:sourceIndexPath];

    // Remove the object we're moving from the array.
    [allFRCObjects removeObject:sourceObject];
    // Now re-insert it at the destination.
    [allFRCObjects insertObject:sourceObject atIndex:[destinationIndexPath row]];

    // All of the objects are now in their correct order. Update each
    // object displayOrder field by iterating through the array.
    int i = 0;
    for (NSManagedObject *mo in allFRCObjects)
    {
        [mo setValue:[NSNumber numberWithInt:i++] forKey:sortProperty];
    }
    //DO NOT SAVE THE MANAGED OBJECT CONTEXT YET


}

- (void)setEditing:(BOOL)editing
{
    [super setEditing:editing];
    if(!editing)
        [self.managedObjectContext save:nil];
}

Ответ 6

Я, наконец, отказался от FetchController в режиме редактирования, так как мне также нужно изменить порядок моих ячеек таблицы. Я бы хотел, чтобы этот пример работал. Вместо этого я продолжал иметь mutablearray, являющийся текущим представлением таблицы, а также сохранение согласованного атрибута orderItem CoreData.

NSUInteger fromRow = [fromIndexPath row]; 
NSUInteger toRow = [toIndexPath row]; 



 if (fromRow != toRow) {

    // array up to date
    id object = [[eventsArray objectAtIndex:fromRow] retain]; 
    [eventsArray removeObjectAtIndex:fromRow]; 
    [eventsArray insertObject:object atIndex:toRow]; 
    [object release]; 

    NSFetchRequest *fetchRequestFrom = [[NSFetchRequest alloc] init];
    NSEntityDescription *entityFrom = [NSEntityDescription entityForName:@"Lister" inManagedObjectContext:managedObjectContext];

    [fetchRequestFrom setEntity:entityFrom];

    NSPredicate *predicate; 
    if (fromRow < toRow) predicate = [NSPredicate predicateWithFormat:@"itemOrder >= %d AND itemOrder <= %d", fromRow, toRow];  
    else predicate = [NSPredicate predicateWithFormat:@"itemOrder <= %d AND itemOrder >= %d", fromRow, toRow];                          
    [fetchRequestFrom setPredicate:predicate];

    NSError *error;
    NSArray *fetchedObjectsFrom = [managedObjectContext executeFetchRequest:fetchRequestFrom error:&error];
    [fetchRequestFrom release]; 

    if (fetchedObjectsFrom != nil) { 
        for ( Lister* lister in fetchedObjectsFrom ) {

            if ([[lister itemOrder] integerValue] == fromRow) { // the item that moved
                NSNumber *orderNumber = [[NSNumber alloc] initWithInteger:toRow];               
                [lister setItemOrder:orderNumber];
                [orderNumber release];
            } else { 
                NSInteger orderNewInt;
                if (fromRow < toRow) { 
                    orderNewInt = [[lister itemOrder] integerValue] -1; 
                } else { 
                    orderNewInt = [[lister itemOrder] integerValue] +1; 
                }
                NSNumber *orderNumber = [[NSNumber alloc] initWithInteger:orderNewInt];
                [lister setItemOrder:orderNumber];
                [orderNumber release];
            }

        }

        NSError *error;
        if (![managedObjectContext save:&error]) {
            NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();  // Fail
        }           

    }                                   

}   

Если у кого-то есть решение, использующее fetchController, отправьте его.

Ответ 7

На самом деле существует гораздо более простой способ использования типа "double" в качестве столбца упорядочения.

Тогда всякий раз, когда вы переупорядочиваете, вам нужно только reset значение атрибута order для переупорядоченного элемента:

reorderedItem.orderValue = previousElement.OrderValue + (next.orderValue - previousElement.OrderValue) / 2.0;

Ответ 8

Итак, потратив некоторое время на эту проблему...!

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

Есть некоторые проблемы с этими подходами:

  • NSFetchedResultsController, связанный с NSMutableArray, не гарантирует, что контекст будет обновлен, поэтому вы можете видеть, что это работает иногда, но не другие.

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

  • Если вы используете строку индекса объекта и имеете разделы, то это не будет вести себя правильно. В некотором коде выше используется только свойство .row, и, к сожалению, это может относиться к нескольким строкам в yt

  • Использование NSFetchedResults Delegate = nil, подходит для простых приложений, но считайте, что вы хотите использовать делегат для записи изменений, которые будут реплицированы в базу данных, тогда вы увидите, что это не будет работать должным образом.

  • Основные данные на самом деле не поддерживают сортировку и упорядочение в том, что делает SQL-база. Решение для цикла for выше хорошо, но действительно должен быть правильный способ упорядочения данных - IOS8? - так что вам нужно пойти на это, ожидая, что ваши данные будут повсюду.

Вопросы, которые люди опубликовали в ответ на эти должности, связаны с множеством этих проблем.

У меня есть простое настольное приложение с разделами для "частичной" работы - все еще остаются необъяснимые поведения пользовательского интерфейса, над которыми я работаю, но я считаю, что у меня это получилось...

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

Это обычный делегат

{
userDrivenDataModelChange = YES;

использует механизм семафора, как описано выше, с структурами возврата if().

NSInteger sourceRow = sourceIndexPath.row;
NSInteger sourceSection = sourceIndexPath.section;
NSInteger destinationRow = destinationIndexPath.row;
NSInteger destinationSection = destinationIndexPath.section;

Не все из них используются в коде, но полезно иметь их для отладки

NSError *error = nil;
NSIndexPath *destinationDummy;
int i = 0;

Окончательная инициализация переменных

destinationDummy = [NSIndexPath indexPathForRow:0 inSection:destinationSection] ;
// there should always be a row zero in every section - although it not shown

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

NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];    
NSManagedObject *currentObject = [self.fetchedResultsController objectAtIndexPath:sourceIndexPath];
NSManagedObject *targetObject = [self.fetchedResultsController objectAtIndexPath:destinationDummy];

Получить контекст и исходные и целевые объекты

Затем этот код создает новый объект, который берет данные из источника и раздела из адресата.

// set up a new object to be a copy of the old one
NSManagedObject *newObject = [NSEntityDescription
                              insertNewObjectForEntityForName:@"List"
                            inManagedObjectContext:context];
NSString *destinationSectionText = [[targetObject valueForKey:@"section"] description];
[newObject setValue:destinationSectionText forKeyPath:@"section"];
[newObject setValue: [NSNumber numberWithInt:9999999] forKey:@"rowIndex"];
NSString *currentItem = [[currentObject valueForKey:@"item"] description];
[newObject setValue:currentItem forKeyPath:@"item"];
NSNumber *currentQuantity =[currentObject valueForKey:@"quantity"] ;
[newObject setValue: currentQuantity forKey:@"rowIndex"];

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

// create a copy of the object for the new location
[context insertObject:newObject];
[context deleteObject:currentObject];
if (![context save:&error]) {
    // Replace this implementation with code to handle the error appropriately.
    // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    abort();
}

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

i = 0;
for (NSManagedObject *mo in [self.fetchedResultsController fetchedObjects] )
{
    [mo setValue:[NSNumber numberWithInt:i++] forKey:@"rowIndex"];
}
if (![context save:&error]) {
    // Replace this implementation with code to handle the error appropriately.
    // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    abort();
}

Установите семафор обратно и обновите таблицу

userDrivenDataModelChange = NO;

[tableView reloadData];

}

Ответ 9

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

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

Не уверен, что это общее решение или даже правильно, но пока это работает.

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath {
    HomeEntity* source_home = [self getHomeEntityAtIndexPath:sourceIndexPath];
    HomeEntity* destination_home = [self getHomeEntityAtIndexPath:destinationIndexPath];
    NSTimeInterval temp = destination_home.createDate;
    destination_home.createDate = source_home.createDate;
    source_home.createDate = temp;

    CoreDataStack * stack = [CoreDataStack defaultStack];
    [stack saveContext];
    [self.tableView reloadData];
}

Ответ 10

Попробуйте взглянуть на учебник Core Data для iPhone здесь. Один из разделов говорит о сортировке (используя NSSortDescriptor).

Вы также можете найти Основные основы баз данных.