12

データソースとしてUITableViewを使用するがあります。NSFetchedResultsController

コアデータストアは、並行して実行される複数のバックグラウンドスレッドで更新されます(各スレッドは独自のスレッドを使用しますNSManagedObjectContext)。

メインスレッドはNSManagedObjectContextDidSaveNotification 通知を監視し、そのコンテキストをで更新しますmergeChangesFromContextDidSaveNotification:

場合によっては、その時点でもう存在しないindexPathを使用してイベントをNSFetchedResultsController送信する ことがあります。NSFetchedResultsChangeUpdate

例:フェッチされた結果コントローラーの結果セットには、4つのオブジェクトを含む1つのセクションが含まれています。最初のオブジェクトは1つのスレッドで削除されます。最後のオブジェクトは別のスレッドで更新されます。次に、次のことが発生することがあります。

  • controllerWillChangeContent:が呼び出されます。
  • controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:はtype = NSFetchedResultsChangeDelete、indexPath.row=0で呼び出されます。
  • controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:はtype = NSFetchedResultsChangeUpdate、indexPath.row=3で呼び出されます。

しかし、フェッチされた結果コントローラーには、現在3つのオブジェクトしか含まれていません。

MyManagedObject *obj = [controller objectAtIndexPath:indexPath]

イベントに従ってテーブルビューセルを更新するNSFetchedResultsChangeUpdate と、例外が発生してクラッシュしNSRangeExceptionます。

ヘルプやアイデアをありがとう!

4

2 に答える 2

16

私は今、私の問題の解決策を見つけました。更新イベントの場合、呼び出す必要はありません

[self.controller objectAtIndexPath:indexPath]

更新されたオブジェクトは、デリゲートにanObjectパラメーターとしてすでに提供されているため-controller:didChangedObject:...です。

したがって、更新されたオブジェクトを直接使用-configureCell:atIndexPath:するメソッドに置き換えました。-configureCell:withObject:これは問題なく機能するようです。

コードは次のようになります。

- (void)configureCell:(UITableViewCell *)cell withObject:(MyManagedObject *)myObj
{
    cell.textLabel.text = myObj.name;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyCellIdentifier"];
    [self configureCell:cell withObject:[self.controller objectAtIndexPath:indexPath]];
    return cell;
}

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath
     forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
    UITableView *tableView = self.tableView;

    switch(type) {
    /* ... */           
    case NSFetchedResultsChangeUpdate:
        [self configureCell:[tableView cellForRowAtIndexPath:indexPath] withObject:anObject];
        break;
    /* ... */           
    }
}
于 2012-07-16T11:28:59.667 に答える
9

これは、コアデータを有効にして新しいマスター/詳細プロジェクトを作成するときに発生するNSFetchedResultsControllerDelegateのAppleのボイラープレートコードのバグのため、実際には非常に一般的です。

- (void)controller:(NSFetchedResultsController *)controller
    didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath
    forChangeType:(NSFetchedResultsChangeType)type
    newIndexPath:(NSIndexPath *)newIndexPath
{
    UITableView *tableView = self.tableView;

    switch(type) {

        case NSFetchedResultsChangeInsert:
            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
                       withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
                       withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeUpdate:
            [self configureCell:[tableView cellForRowAtIndexPath:indexPath]
                  atIndexPath:indexPath];
            break;

        case NSFetchedResultsChangeMove:
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
                       withRowAnimation:UITableViewRowAnimationFade];
            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
                       withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}

解決策1:使用anObject

オブジェクトがすでに提供されているのに、フェッチされた結果コントローラーにクエリを実行し、誤ったインデックスパスを使用するリスクを負うのはなぜですか?Martin Rは、このソリューションも推奨しています。

ヘルパーメソッドconfigureCell:atIndexPath:をインデックスパスの取得から変更された実際のオブジェクトの取得に変更するだけです。

- (void)configureCell:(UITableViewCell *)cell withObject:(NSManagedObject *)object {
    cell.textLabel.text = [[object valueForKey:@"timeStamp"] description];
}

行のセルで、次を使用します。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
    [self configureCell:cell withObject:[self.fetchedResultsController objectAtIndexPath:indexPath]];
    return cell;
}

最後に、アップデートでは以下を使用します。

case NSFetchedResultsChangeUpdate:
    [self configureCell:[tableView cellForRowAtIndexPath:indexPath]
             withObject:anObject];
    break;

解決策2:使用newIndexPath

iOS 7.1以降、NSFetchedResultsChangeUpdateが発生するindexPathと、との両方が渡されます。newIndexPath

cellForRowAtIndexPathを呼び出すときに、デフォルトの実装でのindexPathの使用法を維持するだけで、newIndexPathに送信される2番目のインデックスパスを変更します。

case NSFetchedResultsChangeUpdate:
    [self configureCell:[tableView cellForRowAtIndexPath:indexPath]
            atIndexPath:newIndexPath];
    break;

解決策3:インデックスパスで行をリロードする

Ole Begemannの解決策は、インデックスパスをリロードすることです。セルを構成するための呼び出しを、行を再ロードするための呼び出しに置き換えます。

case NSFetchedResultsChangeUpdate:
    [tableView reloadRowsAtIndexPaths:@[indexPath]
                     withRowAnimation:UITableViewRowAnimationAutomatic];
    break;

この方法には2つの欠点があります。

  1. リロード行を呼び出すことにより、cellForRowを呼び出し、次にdequeueReusableCellWithIdentifierを呼び出します。これにより、既存のセルが再利用され、重要な状態が取り除かれる可能性があります(たとえば、セルがメールボックススタイルでドラッグされている場合)。
  2. 表示されていないセルを誤ってリロードしようとします。Appleの元のコードでは、cellForRowAtIndexPath:nilは「セルが表示されていないかindexPath範囲外の場合」を返します。したがって、リロード行を呼び出す前に、 indexPathsForVisibleRowsを確認する方が適切です。

バグの再現

  1. Xcode6.4のコアデータを使用して新しいマスター/詳細プロジェクトを作成します。
  2. コアデータeventオブジェクトにtitle属性を追加します。
  3. テーブルに複数のレコードを入力します(たとえば、viewDidLoadこのコードを実行する場合)

    NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
    NSEntityDescription *entity = [[self.fetchedResultsController fetchRequest] entity];
    for (NSInteger i = 0; i < 5; i++) {
        NSManagedObject *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];
        [newManagedObject setValue:[NSDate date] forKey:@"timeStamp"];
        [newManagedObject setValue:[@(i) stringValue] forKey:@"title"];
    }
    [context save:nil];
    
  4. タイトル属性を表示するように構成セルを変更します。

    - (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
        NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
        cell.textLabel.text = [NSString stringWithFormat:@"%@ - %@", [object valueForKey:@"timeStamp"], [object valueForKey:@"title"]];
    }
    
  5. 新しいボタンをタップしたときにレコードを追加することに加えて、最後のアイテムを更新します(注:これはアイテムの作成前または作成後に実行できますが、保存が呼び出される前に必ず実行してください!):

    // update the last item
    NSArray *objects = [self.fetchedResultsController fetchedObjects];
    NSManagedObject *lastObject = [objects lastObject];
    [lastObject setValue:@"updated" forKey:@"title"];
    
  6. アプリを実行します。5つのアイテムが表示されます。

  7. 新しいボタンをタップします。新しいアイテムが一番上に追加され、最後のアイテムには「更新されました」というテキストが含まれているはずですが、含まれていないことがわかります。セルを強制的にリロードすると(たとえば、セルを画面からスクロールして外すと)、「更新されました」というテキストが表示されます。
  8. ここで、上記の3つのソリューションのいずれかを実装すると、追加されるアイテムに加えて、最後のアイテムのテキストが「更新済み」に変更されます。
于 2015-08-25T20:04:35.747 に答える