削除の原因がカスケード削除ルールである場合、 prepareForDeleteがモデルを更新するたびに、NSFetchedResultsControllerにバグがあるようです。
暗黙的な削除 (カスケード削除による) は、明示的な削除とは非常に異なる動作をすることを暗示しているようです。
これは本当にバグですか? それとも、なぜこれらの奇妙な結果が表示されるのか説明していただけますか?
プロジェクトの設定
このセクション全体をスキップして、代わりに xcodeproj をダウンロードできます。
マスター/ディテール アプリケーションテンプレートを使用して新しいプロジェクトを作成します。
Eventエンティティに新しい属性を追加します。(NSFetchedResultsController がアイテムの順序を変更することなく属性を更新できるようにするため、これは重要です。そうしないと、
NSFetchedResultsChangeMove
イベントではなくイベントが送信されますNSFetchedResultsChangeUpdate
)。attribute を呼び出して、 にし
hasMovedUp
ますBoolean
。(注: このような属性を作成するのはばかげているように思えるかもしれませんが、これは単なる例であり、このバグを再現するために必要な最小限の手順に減らすように努めました。)新しいエンティティを追加し、それを呼び出します
EventParent
。Eventとの関係を作成し、それを と呼びます
child
。逆の関係も作り、 と呼びますparent
。(注: これは 1 対 1 の関係です。)EventParent をクリックします。その子関係をクリックします。その削除規則をCascadeに設定します。アイデアは、親オブジェクトのみを削除するということです。親が削除されると、その子も自動的に削除されます。
イベントの親関係の削除ルールをNullifyのままにします。
両方のエンティティに対して、Xcode を介して NSManagedObject サブクラスを作成します。
新しいイベントが作成される
insertNewObject:
メソッドでは、対応する親を必ず作成してください。ファイルで、イベントを宣言して、
Event.m
最後のイベントを自動的に割り当てます。hasMovedUp
YES
prepareForDeletion
NSLog(@"Prepare for deletion"); NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Event"]; NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"timeStamp" ascending:NO]; [fetchRequest setSortDescriptors:@[sortDescriptor]]; NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:nil]; NSAssert(results, nil); Event *lastEvent = results.lastObject; NSLog(@"Updating event: %@", lastEvent.timeStamp); lastEvent.hasMovedUp = @YES; [super prepareForDeletion];
Storyboard で、DetailViewController へのセグエを削除します。私たちはそれを必要としません。
および
didChangeObject
の場合、イベントにいくつかのログ ステートメントを追加します。出力させます。NSFetchedResultsChangeDelete
NSFetchedResultsChangeUpdate
indexPath.row
最後に、セルがタップされると、対応する親が削除されるようにします。
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
これを行うには、ファイルに を作成しMasterViewController.m
ます。NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext]; Event *event = [self.fetchedResultsController objectAtIndexPath:indexPath]; EventParent *parent = event.parent; NSLog(@"Deleting event: %@", event.timeStamp); [context deleteObject:parent]; //[context deleteObject:event]; // comment and uncomment this line to reproduce or fix the error, respectively.
ここまでの設定のまとめ:
- NSFetchedResultsController にはあまり触れません。イベントを観察して表示できるようにします。
- EventParent を削除するたびに、対応する Event を削除する必要があります。
- さらにひねりを加えて
hasMovedUp
、イベントが削除されるたびにプロパティが更新されるようにします。
バグの再現
アプリを実行する
プラスボタンを 2 回タップして、2 つのレコードを作成します。
一番上のレコードをタップして、アプリがクラッシュするのを確認します (注: 95% の確率でクラッシュします。クラッシュしない場合は、クラッシュするまでアプリを再起動してください)。便利な NSLog を次に示します。
2013-07-09 13:38:26.984 ReproNFC_PFD_bug[9518:11603] Deleting event: 2013-07-09 20:28:30 +0000 2013-07-09 13:38:26.986 ReproNFC_PFD_bug[9518:11603] Prepare for deletion 2013-07-09 13:38:26.987 ReproNFC_PFD_bug[9518:11603] Updating event: 2013-07-09 02:48:49 +0000 2013-07-09 13:38:26.989 ReproNFC_PFD_bug[9518:11603] Delete detected on row: 0 2013-07-09 13:38:26.990 ReproNFC_PFD_bug[9518:11603] Update detected on row: 1
[context deleteObject:event]
上記の行のコメントを外します。アプリを実行して、クラッシュしなくなったことを確認します。ログ:
2013-07-09 13:20:19.917 ReproNFC_PFD_bug[8997:11603] Deleting event: 2013-07-09 20:20:03 +0000 2013-07-09 13:20:19.919 ReproNFC_PFD_bug[8997:11603] Prepare for deletion 2013-07-09 13:20:19.921 ReproNFC_PFD_bug[8997:11603] Delete detected on row: 0 2013-07-09 13:20:19.924 ReproNFC_PFD_bug[8997:11603] Updating event: 2013-07-09 02:48:49 +0000 2013-07-09 13:20:19.925 ReproNFC_PFD_bug[8997:11603] Update detected on row: 0
ログでは次の 2 つの点が異なります。
次のイベントを更新する前に、削除が検出されます。
行 1 (正しくない行) ではなく、行 0 (正しい行) で更新が行われます。0 が正しい数である理由の説明を読んでください。
(注: エラーが発生すると予想されるが実際には発生しない 5% の時間であっても、ログ イベントはまったく同じ順序で出力されます。)
例外
の次の行で例外が発生しますconfigureCell:atIndexPath:
。
NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
例外が発生する理由は、存在しなくなった行で更新が検出されたためです (1)。例外が発生しない場合、更新は正しい行 (0) で検出されることに注意してください。これは、一番上の行が削除され、一番下の行がインデックス 0 になっているためです。
発生する例外は次のとおりです。
CoreData: エラー: 重大なアプリケーション エラーです。コア データの変更処理中に例外がキャッチされました。これは通常、NSManagedObjectContextObjectsDidChangeNotification のオブザーバー内のバグです。*** -[_PFBatchFaultingArray objectAtIndex:]: インデックス (19789522) が境界 (2) を超えており、userInfo (null)
.
* キャッチされていない例外 'NSRangeException' が原因でアプリを終了しています。
含意
これは、カスケード削除ルールに依存することは、自分でオブジェクトを明示的に削除することと同じではないことを示唆しているようです。
言い換えると...
これ:
[context deleteObject:parent];
// parent will auto-delete the corresponding Event via a cascade rule
…これと同じではありません:
[context deleteObject:parent];
[context deleteObject:event];
回避策
2013 年 6 月 9 日更新:
Xcodeprojが更新され、利用可能なさまざまな回避策のいくつかのステートメントが含まれるようになりました ( Event.hファイル内)。バグを再現するには、3 つすべてを未定義のままにします。これらのいずれかを定義して、特定の回避策が実装されていることを確認します。これまでのところ、A、B、および C の 3 つの回避策があります。#define
A: 明示的に削除を呼び出す
このソリューションは、既に上で説明したものと重複していますが、完全を期すために含まれています。
カスケード削除に依存せず、代わりに削除を自分で呼び出すことにより、すべてが正常に機能します。
// (CUSTOMIZATION_POINT A)
[context deleteObject:parent]; // A1: this line should always run
#ifdef Workaround_A
[context deleteObject:event]; // A2: this line will fix the bug
#endif
ログ:
2013-07-09 13:20:19.917 ReproNFC_PFD_bug[8997:11603] Deleting event: 2013-07-09 20:20:03 +0000
2013-07-09 13:20:19.919 ReproNFC_PFD_bug[8997:11603] Prepare for deletion
2013-07-09 13:20:19.921 ReproNFC_PFD_bug[8997:11603] Delete detected on row: 0
2013-07-09 13:20:19.924 ReproNFC_PFD_bug[8997:11603] Updating event: 2013-07-09 02:48:49 +0000
2013-07-09 13:20:19.925 ReproNFC_PFD_bug[8997:11603] Update detected on row: 0
B: @MartinR の推奨事項を使用:
パラメーターを無視し、メソッドでパラメーターindexPath
のみを使用することで、問題を回避できます。anObject
didChangeObject:
case NSFetchedResultsChangeUpdate:
NSLog(@"Update detected on row: %d", indexPath.row);
// (CUSTOMIZATION_POINT B)
#ifndef Workaround_B
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath]; // B1: causes bug
#else
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] withObject:anObject]; // B2: doesn't cause bug
#endif
break;
ただし、ログにはまだ順不同で表示されます。
2013-07-09 13:24:43.662 ReproNFC_PFD_bug[9101:11603] Deleting event: 2013-07-09 20:24:42 +0000
2013-07-09 13:24:43.663 ReproNFC_PFD_bug[9101:11603] Prepare for deletion
2013-07-09 13:24:43.666 ReproNFC_PFD_bug[9101:11603] Updating event: 2013-07-09 02:48:49 +0000
2013-07-09 13:24:43.667 ReproNFC_PFD_bug[9101:11603] Delete detected on row: 0
2013-07-09 13:24:43.667 ReproNFC_PFD_bug[9101:11603] Update detected on row: 1
これにより、このソリューションがコードの他の部分で関連する問題を引き起こす可能性があると私は信じています。
C: prepareForDelete で 0 秒の遅延を使用する:
削除の準備でゼロ秒の遅延の後にオブジェクトを更新すると、バグが回避されます。
- (void)updateLastEventInContext:(NSManagedObjectContext *)context {
// warning: do not call self.<anything> in this method when it is called with a delay, since the object would have already been deleted
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Event"];
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"timeStamp" ascending:NO];
[fetchRequest setSortDescriptors:@[sortDescriptor]];
NSArray *results = [context executeFetchRequest:fetchRequest error:nil];
NSAssert(results, nil);
Event *lastEvent = results.lastObject;
NSLog(@"Updating event: %@", lastEvent.timeStamp);
lastEvent.hasMovedUp = @YES;
}
- (void)prepareForDeletion {
NSLog(@"Prepare for deletion");
// (CUSTOMIZATION_POINT C)
#ifndef Workaround_C
[self updateLastEventInContext:self.managedObjectContext]; // C1: causes the bug
#else
[self performSelector:@selector(updateLastEventInContext:) withObject:self.managedObjectContext afterDelay:0]; // C2: doesn't cause the bug
#endif
[super prepareForDeletion];
}
さらに、ログの順序は正しいように見えるため、NSFetchedResultsController で indexPath の呼び出しを再開できます (つまり、回避策 B を使用する必要はありません)。
2013-07-09 13:27:38.308 ReproNFC_PFD_bug[9196:11603] Deleting event: 2013-07-09 20:27:37 +0000
2013-07-09 13:27:38.309 ReproNFC_PFD_bug[9196:11603] Prepare for deletion
2013-07-09 13:27:38.310 ReproNFC_PFD_bug[9196:11603] Delete detected on row: 0
2013-07-09 13:27:38.319 ReproNFC_PFD_bug[9196:11603] Updating event: 2013-07-09 02:48:49 +0000
2013-07-09 13:27:38.320 ReproNFC_PFD_bug[9196:11603] Update detected on row: 0
self.timeStamp
ただし、これは、オブジェクトがその時点ですでに削除されているため、メソッドなどで にアクセスできないことを意味しますupdateLastEventInContext:
(これは、親オブジェクトを削除するための呼び出しの直後にコンテキストを保存することを前提としています)。