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

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

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


person Boon    schedule 03.07.2009    source источник
comment
См. Пример кода здесь: stackoverflow.com/a/15625897/308315   -  person iwasrobbed    schedule 27.03.2013


Ответы (10)


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), и он отлично работает.

person Aleksandar Vacić    schedule 06.01.2010
comment
+1 Это было для меня ключом к реализации changeIsUserDriven, упомянутого в документации. Спасибо. - person hanleyp; 14.01.2010
comment
Также см. stackoverflow.com/questions/1648223/ - person gerry3; 29.01.2010
comment
Прекрасно работает, не забудьте сделать то же самое для - (void) controller: (NSFetchedResultsController *) controller didChangeSection: (id ‹NSFetchedResultsSectionInfo›) - person Peter Johnson; 04.12.2012
comment
Отлично работает ... Мне потребовались часы, пытаясь понять, как работает FetchResultsController ... Это немного сложно. Apple должна улучшить этот API ... Я получаю здесь код индекса заказа обновленных моделей: cimgf.com/2010/06/05/re-ordering-nsfetchedresultscontroller. Могут быть небольшие накладные расходы, но они просты в использовании и работают стабильно. - person Cullen SUN; 19.12.2012
comment
Вместо добавления дополнительного логического флага вы также можете установить для делегата NSFetchedResultsController значение nil, пока вы применяете изменения, а затем обратно к self. - person eploko; 05.02.2013
comment
Отлично работает :) Спасибо! - person Emil Marashliev; 26.02.2013
comment
Вот и все! Большое спасибо! Потребовалось время, чтобы исправить это, пока я не нашел это! - person horseshoe7; 06.05.2015
comment
@eploko, если вы установите делегата в ноль, когда вы вернете делегата? - person user1046037; 05.03.2018

Вот быстрый пример, показывающий способ сбросить полученные результаты в 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's current position
    [sortedPlaylists removeObjectAtIndex:sourceIndexPath.row];

    // Insert it at it's 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];
}
person iwasrobbed    schedule 25.03.2013

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

Могут возникнуть патологические случаи, когда строка слишком длинная, float слишком мал или в 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.

person dk.    schedule 19.12.2009

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

- (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's 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];
}
person Arie Litovsky    schedule 23.06.2012
comment
Если пользователь перемещается более одного раза за один период редактирования, массив allFRCObjects вернется в исходное состояние в начале последовательных перемещений. Поэтому, когда пользователь нажимает кнопку «Готово», может быть сохранен только последний ход. - person David J. Y. Tang; 23.06.2014

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

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

не стесняйтесь раскошелиться :)

person Stephan    schedule 01.02.2012
comment
Хорошая работа и спасибо. - person Ben; 24.01.2014
comment
Спасибо ... приятно видеть положительный отзыв - person Stephan; 26.01.2014

На самом деле, есть более простой способ - использовать тип «double» в качестве столбца для упорядочивания.

Затем всякий раз, когда вы переупорядочиваете, вам нужно только ВСЕГДА сбрасывать значение атрибута order для переупорядоченного элемента:

reorderedItem.orderValue = previousElement.OrderValue + (next.orderValue - previousElement.OrderValue) / 2.0;
person Andrew    schedule 10.11.2011
comment
Я думаю, вам нужно различать движения вверх и вниз, особенно для индекса 0 и последнего объекта. - person Stephan; 02.02.2012
comment
Вы также рискуете получить недостаточное количество данных, если не нормализуете при необходимости. - person MdaG; 22.02.2012
comment
double и float медленнее, чем целое при сравнении. попробуйте вместо этого использовать целое число. - person flypig; 30.12.2013

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

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, опубликуйте его.

person Community    schedule 20.07.2009

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

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

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

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

  2. Подход «копирование и удаление» для обмена объектами также сложно предсказать. В другом месте я нашел ссылки на непредсказуемое поведение при обращении к объекту, который был удален в контексте.

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

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

  5. Core Data на самом деле не поддерживает сортировку и упорядочение, как это делает правильная база данных 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's 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();
}

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

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];

}

person user3519965    schedule 10.04.2014

Вот то, что я делаю, похоже, работает. Для каждой сущности у меня есть 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];
}
person Todd Hoff    schedule 30.01.2015

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

Вы также можете найти основы Core Data страница, чтобы быть полезной.

person Timothy Walters    schedule 03.07.2009
comment
Но это не сортировка, а переупорядочение. Сортировка проста, но переупорядочение потенциально связано с большим количеством обновлений, если я не использую подход с двойными связями, который я не уверен, как перевести в CoreData. - person Boon; 03.07.2009
comment
Есть ли причина, по которой вы не хотите повторно запрашивать новую инструкцию Sort? В любом случае он обращается к локальным данным, так что это не значит, что это сильно снижает производительность. - person Timothy Walters; 13.07.2009
comment
На самом деле дело не в том, чтобы избегать сортировки. Главный вопрос: как мне в первую очередь реализовать заказ? Если я добавляю индекс заказа в схему, я сталкиваюсь с ситуацией, когда мне приходится делать много перерасчетов, когда я переупорядочиваю строку. Просто представьте это с изменением порядка в массиве, и вы поймете, что я имею в виду - когда вы перемещаете элемент массива в другую позицию, все, что находится после него, также должно быть перемещено. - person Boon; 18.07.2009