Programing

CoreData 레코드의 재정렬을 구현하는 방법은 무엇입니까?

crosscheck 2020. 11. 24. 07:40
반응형

CoreData 레코드의 재정렬을 구현하는 방법은 무엇입니까?


iPhone 앱에 CoreData를 사용하고 있지만 CoreData는 레코드를 다시 정렬 할 수있는 자동 방법을 제공하지 않습니다. 주문 정보를 저장하기 위해 다른 열을 사용하려고 생각했지만 주문 색인에 연속 번호를 사용하는 데 문제가 있습니다. 많은 데이터를 다루는 경우 레코드를 재정렬하면 잠재적으로 주문 정보에 대한 많은 레코드를 업데이트 할 수 있습니다 (배열 요소의 순서를 변경하는 것과 같습니다).

효율적인 주문 체계를 구현하는 가장 좋은 방법은 무엇입니까?


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)에서 이것을 구현했으며 제대로 작동합니다.


다음은 가져온 결과를 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];
}

늦은 응답 : 아마도 정렬 키를 문자열로 저장할 수 있습니다. 두 개의 기존 행 사이에 레코드를 삽입하는 것은 문자열에 추가 문자를 추가하여 간단하게 수행 할 수 있습니다. 예를 들어 행 "A"와 "B"사이에 "AM"을 삽입합니다. 재주문이 필요하지 않습니다. 4 바이트 정수에서 부동 소수점 숫자 또는 간단한 비트 산술을 사용하여 비슷한 아이디어를 얻을 수 있습니다. 인접한 행 사이의 중간에 정렬 키 값이있는 행을 삽입합니다.

문자열이 너무 길거나 플로트가 너무 작거나 int에 더 이상 공간이없는 병리학적인 경우가 발생할 수 있지만 엔티티 번호를 다시 매기고 새로 시작할 수 있습니다. 드물게 모든 레코드를 스캔하고 업데이트하는 것이 사용자가 재정렬 할 때마다 모든 개체에 오류를 발생시키는 것보다 훨씬 낫습니다.

예를 들어 int32를 고려하십시오. 높은 3 바이트를 초기 순서로 사용하면 두 행 사이에 최대 256 개의 행을 삽입 할 수있는 기능과 함께 거의 1,700 만 행이 제공됩니다. 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이됩니다.


이중 값으로 @andrew / @dk의 접근 방식을 구현했습니다.

github 에서 UIOrderedTableView찾을 수 있습니다 .

그것을 포크 자유롭게 느끼십시오 :)


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

사실, 훨씬 더 간단한 방법이 있습니다. "double"유형을 주문 열로 사용하는 것입니다.

그런 다음 재주문 할 때마다 재주문 된 항목의 주문 속성 값을 재설정하면됩니다.

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

테이블 셀을 재정렬해야하므로 편집 모드에서 FetchController를 포기했습니다. 작동하는 예를보고 싶습니다. 대신 테이블의 현재 뷰가되는 mutablearray를 유지하고 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를 사용하는 솔루션이 있으면 게시하십시오.


그래서이 문제에 시간을 보냈다 ...!

위의 답변은 훌륭한 빌딩 블록이며 그것들이 없었다면 나는 길을 잃었을 것입니다. 그러나 다른 응답자들과 마찬가지로 나는 그것들이 부분적으로 만 작동한다는 것을 발견했습니다. 이를 구현하면 한두 번 작동하고 오류가 발생하거나 이동하면서 데이터가 손실된다는 것을 알 수 있습니다. 아래의 대답은 완벽하지 않습니다. 그것은 많은 늦은 밤, 시행 착오의 결과입니다.

이러한 접근 방식에는 몇 가지 문제가 있습니다.

  1. NSMutableArray에 연결된 NSFetchedResultsController는 컨텍스트가 업데이트된다는 것을 보장하지 않으므로 이것이 때때로 작동하지만 다른 것은 작동하지 않는 것을 볼 수 있습니다.

  2. 개체를 교체하기위한 복사 후 삭제 방법도 예측하기 어려운 동작입니다. 컨텍스트에서 삭제 된 개체를 참조 할 때 예측할 수없는 동작에 대한 참조를 다른 곳에서 찾았습니다.

  3. If you use the object index row and have sections, then this won't behave properly. Some of the code above uses just the .row property and unfortunately this could refer to more than one row in a yt

  4. Using NSFetchedResults Delegate = nil, is ok for simple applications, but consider that you want to use the delegate to capture changes that will be replicated to a database then you can see that this won't work properly.

  5. Core Data doesn't really support sorting and ordering in the way that a proper SQL database does. The for loop solution above is good, but there should really be a proper way of ordering data - IOS8? - so you need to go into this expecting that your data will be all over the place.

The issues that people have posted in response to these posts relate to a lot of these issues.

I have got a simple table app with sections to 'partially' work - there are still unexplained UI behaviours that I'm working on, but I believe that I have got to the bottom of it...

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

This is the usual delegate

{
userDrivenDataModelChange = YES;

uses the semaphore mechanism as described above with the if()return structures.

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

Not all of these are used in the code, but it's useful to have them for debugging

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

Final initialisation of variables

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

I use a row 0 in each section that is hidden, this stores the section name. This allows the section to be visible, even when there are no 'live records in it. I use row 0 to get the section name. The code here is a bit untidy, but it does the job.

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

Get the context and source and destination objects

This code then creates a new object which is takes the data from the source, and the section from the destination.

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

Now create a new object and save the context - this is cheating the move operation - you might not get the new record in exactly the place it was dropped - but at least it will be in the right section.

// 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();
}

Now do the for loop update as described above. Note that the context is saved before I do this - no idea why this is needed, but it didn't work properly when it wasn't!

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();
}

Set the semaphore back and update the table

userDrivenDataModelChange = NO;

[tableView reloadData];

}


Here's what I'm doing that seems to work. For every entity I have a createDate that is used to sort the table by when it was created. It also acts as a unique key. So on the move all I do is swap the the source and destination dates.

I would expect the table to be properly ordered after doing the saveContext, but what happens is the two cells just lay on top of each other. So I reload the data and the order is corrected. Starting the app from scratch shows the records still in the proper order.

Not sure it's a general solution or even correct, but so far it seems to work.

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

Try having a look at the Core Data tutorial for iPhone here. One of the sections there talk about sorting (using NSSortDescriptor).

You may also find the Core Data basics page to be useful.

참고URL : https://stackoverflow.com/questions/1077568/how-to-implement-re-ordering-of-coredata-records

반응형