これは、コアデータを有効にして新しいマスター/詳細プロジェクトを作成するときに発生する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つの欠点があります。
- リロード行を呼び出すことにより、cellForRowを呼び出し、次にdequeueReusableCellWithIdentifierを呼び出します。これにより、既存のセルが再利用され、重要な状態が取り除かれる可能性があります(たとえば、セルがメールボックススタイルでドラッグされている場合)。
- 表示されていないセルを誤ってリロードしようとします。Appleの元のコードでは、cellForRowAtIndexPath:
nil
は「セルが表示されていないかindexPath
範囲外の場合」を返します。したがって、リロード行を呼び出す前に、 indexPathsForVisibleRowsを確認する方が適切です。
バグの再現
- Xcode6.4のコアデータを使用して新しいマスター/詳細プロジェクトを作成します。
- コアデータ
event
オブジェクトにtitle属性を追加します。
テーブルに複数のレコードを入力します(たとえば、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];
タイトル属性を表示するように構成セルを変更します。
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = [NSString stringWithFormat:@"%@ - %@", [object valueForKey:@"timeStamp"], [object valueForKey:@"title"]];
}
新しいボタンをタップしたときにレコードを追加することに加えて、最後のアイテムを更新します(注:これはアイテムの作成前または作成後に実行できますが、保存が呼び出される前に必ず実行してください!):
// update the last item
NSArray *objects = [self.fetchedResultsController fetchedObjects];
NSManagedObject *lastObject = [objects lastObject];
[lastObject setValue:@"updated" forKey:@"title"];
アプリを実行します。5つのアイテムが表示されます。
- 新しいボタンをタップします。新しいアイテムが一番上に追加され、最後のアイテムには「更新されました」というテキストが含まれているはずですが、含まれていないことがわかります。セルを強制的にリロードすると(たとえば、セルを画面からスクロールして外すと)、「更新されました」というテキストが表示されます。
- ここで、上記の3つのソリューションのいずれかを実装すると、追加されるアイテムに加えて、最後のアイテムのテキストが「更新済み」に変更されます。