web-dev-qa-db-ja.com

コアデータを使用したUITableViewの再配置

重複の可能性:
CoreDataレコードの並べ替えを実装する方法は?

セルがfetchedResultsControllerを使用している場合(つまり、Core Dataと組み合わせて)、tableView内のセルの移動/再配置を処理する方法を示すコードサンプルを見つけようとしています。 moveRowAtIndexPath:データソースへの呼び出しを取得していますが、テーブル/データに変更を正しく認識させるための黒魔術の適切な組み合わせが見つかりません。

たとえば、行0を行2に移動してから放すと、正しく「見えます」。次に、「完了」をクリックします。行0を埋めるために上にスライドした行(1)は、編集モードの外観(マイナスアイコンと移動アイコン)のままですが、下の他の行はスライドして通常の外観に戻ります。次に下にスクロールすると、行2(元々は0、覚えていますか?)が一番上に近づくと、完全に消えます。

WTF。どういうわけかfetchedResultsControllerを無効にする必要がありますか? nilに設定すると、クラッシュします。代わりにリリースする必要がありますか?私は雑草の中にいますか?

これが私が現在そこに持っているものです...

- (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];
    }
}
23
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);
    }
}
7
Greg Combs

通常、そのようなアーティファクトが発生しているのを見ると、UIが新しい位置にアニメーション化され、それについて通知されます。モデルに対して行った更新は、次にグリッチが発生する状態を正しく反映していません。ビューは、更新のためにモデルを参照する必要があります。

その方法で何をすべきかを正確に理解していないと思います。 UIが変更され、それに応じてモデルを変更する必要があるために呼び出されます。以下のコードは、結果がすでに新しい順序になっていることを前提としており、何らかの理由で順序フィールドをリセットする必要があります。

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

欠点は、基になるモデルの順序を実際に変更していないことです。これらのindexPathはUITableViewControllerからのものであり、ユーザーがそれらの間をスポットにドラッグしたため、それに応じて基になるデータを更新する必要があることを示しています。ただし、fetchedResultsControllerは常にソート順であるため、これらのプロパティを変更するまで、何も移動しません。

問題は、それらが移動されていないことです。(sortableプロパティを調整することによって)それらを移動する必要があることを通知するように呼び出されています。あなたは本当にもっと何かをする必要があります:

NSNumber *targetOrder = [fetchedResultsController objectAtIndexPath:toIndexPath];
LinkObj *link = [fetchedResultsController objectAtIndexPath:FromPath];
link.order = targetOrder;

これにより、オブジェクトが並べ替えられ、インデックスが移動した可能性があることに注意して、シフトアップする必要がある他のオブジェクトの注文番号を調べてクリーンアップします。

8
Louis Gerbarg

最良の答えは、実際には、質問に対するClintHarrisのコメントにあります。

http://www.cimgf.com/2010/06/05/re-ordering-nsfetchedresultscontroller

簡単に要約すると、重要な部分は、再配置しようとしているオブジェクトの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ドキュメントには、重要なヒントも含まれています。

https://developer.Apple.com/library/ios/#documentation/CoreData/Reference/NSFetchedResultsControllerDelegate_Protocol/Reference/Reference.html

これは CoreDataレコードの並べ替えを実装する方法は? にも記載されています。

Appleドキュメントを引用するには:

ユーザー主導の更新

一般に、NSFetchedResultsControllerは、モデルレイヤーでの変更に応答するように設計されています。ユーザーがテーブルの行を並べ替えることを許可する場合、デリゲートメソッドの実装ではこれを考慮に入れる必要があります。

通常、ユーザーがテーブルの行を並べ替えることを許可する場合、モデルオブジェクトにはそのインデックスを指定する属性があります。ユーザーが行を移動すると、それに応じてこの属性が更新されます。ただし、これには、コントローラーに変更を認識させるという副作用があるため、そのデリゲートに更新を通知します(controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:を使用)。 「一般的な使用法」に示されているこのメソッドの実装を単に使用する場合、デリゲートはテーブルビューを更新しようとします。ただし、テーブルビューは、ユーザーの操作により、すでに適切な状態になっています。

したがって、一般に、ユーザー主導の更新をサポートする場合、移動がユーザーによって開始された場合はフラグを設定する必要があります。デリゲートメソッドの実装では、フラグが設定されている場合、メインメソッドの実装をバイパスします。例えば:

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

    if (!changeIsUserDriven) {
        UITableView *tableView = self.tableView;
        // Implementation continues...
5
JosephH

テーブルビューで行を移動すると、実際には他の行のブロック(少なくとも1つの行で構成される)を他の方向に移動します同時に。秘訣は、このブロックと移動されたアイテムの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);
    }
}
5
Tom

これは、そのような機能を実装するための非常に難しい方法です。はるかに簡単でエレガントな方法がここにあります: ITableView Core Dataの並べ替え

1
AlexS

[<> isEditing]を使用して、テーブルの編集を有効にするかどうかを決定できます。次のステートメントを使用して提案されているように遅延させるのではなく

[テーブルperformSelector:@selector(reloadData)withObject:nil afterDelay:0.02];

1

申し訳ありませんが、グレッグ、私は何か間違ったことをしていると確信していますが、あなたの答えは私にはうまくいきません。

すべてのオブジェクトが正しく検証されますが、編集モードを終了すると、行の1つがフリーズし(編集コントロールが消えません)、その後セルが正しく応答しません。

たぶん私の問題は、あなたが設定したmovingプロパティ(self.movi​​ng = YES)の使い方がわからないことです。これを明確にしていただけませんか。どうもありがとうございました。

ホルヘ

1
Jorge Ortiz
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath{
    [self.pairs exchangeObjectAtIndex:sourceIndexPath.row withObjectAtIndex:destinationIndexPath.row];
    [self performSelector:@selector(reloadData) withObject:nil afterDelay:0.02];
}

- (void)reloadData{
    [table reloadData];
}

テーブルの移動中はテーブルをリロードできません。しばらくしてからリロードすれば問題ありません。

1
Gregory Ray