2

私は、CS193P スタンフォード クラスから CoreDataTableViewController を拡張する UITableViewController を備えたかなり標準的なアプリを持っています (これは、NSFetchedResultsControllerDelegate ドキュメントのすべてのボイラープレート コードを使用して NSFetchedResultsControllerDelegate を実装する UITableViewController の単なる拡張です)。

とにかく、私のテーブルにはアイテムのリストが表示されます。アイテムpositionには、整数 (Core Data の NSNumber) であるというプロパティがあり、NSFetchedResultsController は、位置でソートするために NSSortDescriptor で設定されます。

これは通常は機能します。テーブルを開くと、performFetch が実行され、アイテムが正しい順序で表示されます。デバッグするログ メッセージをいくつか追加しました。

fetching Item with pedicate: parentList.guid == "123" and sort: (position, ascending, BLOCK(0x6bf1ca0))
fetched item: aaaaa has array:pos = 0 : 0
fetched item: bbbb has array:pos = 1 : 1
fetched item: cccc has array:pos = 2 : 2
fetched item: dddddd has array:pos = 3 : 3

最初の行が示しているのは、performFetch が、GUID でフィルタリングする述語と、位置でソートするソート記述子で発生していることです。次の行は、フェッチ後に NSFetchedResultsController から fetchedObjects をループするときにログに記録されます。最初に項目名 (aaaa、bbbb など) が表示され、次に fetchedObjects 配列内の配列位置、次に position プロパティの値が表示されます。ずらりと並んでいる様子がわかります。

問題は、新しいアイテムを追加してから親ビューに戻ってから、再びリストに進むときに発生します。新しいアイテムが正しい位置 (最後) に追加されます。しかし、前後に移動すると、いくつかのアイテムが順不同になっています。

最初は、フェッチが再度実行されていないか、sortDescriptor が欠落している可能性があると考えていましたが、ロギングはフェッチ行われていることを示しており、物事が順不同であることがわかります。

 fetching Item with pedicate: parentList.guid == "123" and sort: (position, ascending, BLOCK(0x6bf1ca0))
 fetched item: bbbb has array:pos = 0 : 1     <= BAD
 fetched item: cccc has array:pos = 1 : 2     <= BAD
 fetched item: aaaaa has array:pos = 2 : 0    <= BAD
 fetched item: dddddd has array:pos = 3 : 3
 fetched item: eeee has array:pos = 4 : 4

そこを参照してください: 配列項目 0 の位置が 1 で、配列項目 2 の位置が 0 で、配列項目 1 が 2 であることに注意してください。これは実際にはフェッチの直後にフェッチされたオブジェクトであり、fetchRequestcontroller の sortDescriptor と述語が明らかに正しいため、どうしてこれが可能なのでしょうか?

最初はテーブル ビューの問題かもしれないと思っていましたが、fetchObjects の後にこのデバッグ ログを追加したので、それがフェッチの結果であることがわかりました。

また、NSNumbers を自動的に並べ替えることができないのではないかと考えたので、整数値で並べ替える独自のコンパレータを追加しました。しかし違いはありません。

もう一度行ったり来たりすると、次のフェッチで正しい順序に戻されることに注意してください。後続のすべてのフェッチも同様です。ロード後に発生するのはこれだけです。

何か案は?

[アップデート]

コメントでの有益な議論の後 (興味を持ってくれた @MartinR と @tc に感謝します)、物事を少し単純化し、何が起こっているかを示すコードを追加しました。単純化:

  • シンプルなNSStringであるため、アイテム「タイトル」でソートしています。
  • 新しい項目を作成するために子 NSManagedObjectContext を使用しなくなりました - それらはリストと同じ MOC で直接作成され、すぐに (そして同期的に) 保存されます

デモ用のコードの追加: 基本的なセットアップは、アイテムのリストのリストです。Todo アプリの標準的なもの。したがって、私の CoreData Model にはリストとアイテムが含まれています。すべてのリストにはアイテムのセット (1 から多数) があり、すべてのアイテムには親リストへの参照があります。

UI は 2 つの TVC: ListOfListsTVC で、リスト名をクリックすると、ListOfItemsTVC に移動します。

アイテムのリストは異なるリストに使用されるため、新しいリストが設定されるたびに完全に新しい FRC が設定されます。それはここで起こります:

- (void)setupFetchedResultsController
{
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"item"];

    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"parentList.guid = %@", self.list.guid];
    request.predicate = predicate;


    NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"title" ascending:YES];
    request.sortDescriptors = [NSArray arrayWithObject:sortDescriptor];

    self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request                                                                    managedObjectContext:self.list.managedObjectContext sectionNameKeyPath:nil
                                                cacheName:nil];
    self.debug = YES;

}    

これself.fetchedResultsControllerは、cs193p スタンフォード コースからの逐語的な CoreDataTableViewController スーパークラスを呼び出します。

- (void)setFetchedResultsController:(NSFetchedResultsController *)newfrc
{
self.debug = YES;
NSFetchedResultsController *oldfrc = _fetchedResultsController;
if (newfrc != oldfrc) {
    _fetchedResultsController = newfrc;
    newfrc.delegate = self;
    if ((!self.title || [self.title isEqualToString:oldfrc.fetchRequest.entity.name]) && (!self.navigationController || !self.navigationItem.title)) {
        self.title = newfrc.fetchRequest.entity.name;
    }
    if (newfrc) {
        if (self.debug) NSLog(@"[%@ %@] %@", NSStringFromClass([self class]), NSStringFromSelector(_cmd), oldfrc ? @"updated" : @"set");
        [self performFetch]; 
    } else {
        if (self.debug) NSLog(@"[%@ %@] reset to nil", NSStringFromClass([self class]), NSStringFromSelector(_cmd));
        [self.tableView reloadData];
    }
}
}

主にデバッグ情報ですが、重要なステートメントは次のとおりです。 1. プロパティを設定します。2. delegateFRC をこの TVC (自己) に設定し、3. 即時フェッチを実行します。

performFetchは同じ CoreDataTableViewController クラスにあり、上にリストしたすべてのデバッグ情報 (アイテムの名前、fetchedObjects 配列内の位置、および位置の値) をダンプします。(これは実際にはリストをフェッチする一般的なメソッドですが、追加のデバッグ情報を取得するために Item クラスのテストを入れています) ここではリストしませんが、重要なステートメントは次のとおりです。

NSError *error;
[self.fetchedResultsController performFetch:&error];

// Debug:
NSArray *obs = [self.fetchedResultsController fetchedObjects];
// log all the debug info about the items in the fetched array to prove they're not sorted
// ...


[self.tableView reloadData];

したがって、基本的には、テーブルをフェッチしてリロードします。

データにアイテムのリストがある場合、これはアプリを最初に起動したときに機能するようです。失敗しているように見えるのは、新しいアイテムを追加した後です。NewItemTVC という別の静的 TVC でこれを行い、デリゲートの代わりに、ブロック内のコールバックを使用してアイテムを保存します。しかし、効果は同じです。すべて同期です。これがListOfItemsTCVに保存するための私のブロックです

newItemTVC.saveCancelBlock2 = ^ (BOOL save, NSDictionary *descriptor) {
    if (save) {

        NSManagedObjectContext *moc = self.fetchedResultsController.managedObjectContext;
        Item *newItem = [Item itemWithDescriptor:itemDescriptor inManagedObjectContext:moc];

        // here's where I set the position but ignore this for now because
        // I'm sorting on "title" to debug and it has the same problem
        NSInteger newPosition = self.list.lastPosition + 1;
        newItem.position = [NSNumber numberWithInteger:newPosition];

        // and finally add it to the list
        [self.list addItemsObject:newItem];

        NSError *error = nil;
        BOOL saved = [moc save:&error];
        if (!saved) {
            NSLog(@"Unresolved error saving after adding item to parent %@, %@", error, [error userInfo]);
        }
    }

ここで、アイテムを保存した後、NewItemTVC がポップし、ListOfItems がリロードされ、フェッチが実行され、正しい順序になることがありますが、通常はそうではありません。この場合、フェッチは viewWillAppear で実行されます。(以前はそうではありませんでしたが、デバッグ中にこれも追加しました。現在、viewWillDisappear はデリゲートを nil に設定し、NewItemTVC をポップすると、FRC のデリゲートを TVC に戻した後、このコードが新しいフェッチを実行します)これは、リストのリストから進むときにデリゲートを設定したり、フェッチを実行したりしないことに注意してください。これは、list プロパティを設定すると、デリゲートが設定され、フェッチが実行されるためです。したがって、実際には、NewItemTVC をポップして、viewWillAppear でフェッチを実行することから返されるのは、並べ替えが正しく表示されない最初の例です。

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    if (self.fetchedResultsController != nil && self.fetchedResultsController.delegate !=  self) {
        self.fetchedResultsController.delegate = self;
        [self performFetch];
        [self.tableView reloadData];
  }
}

本当にうまくいかないのは、[戻る] を押して ListOfListsTVC を表示し、もう一度リストを押して同じ ListOfItemsTVC に戻るときです (リストが 1 つでも 12 個でもかまいません)。初めてこれを行うと、アイテムは常に故障しています。時にはそれを 4、5 回繰り返すことができますが、それでも順番が狂いますが、何度も行ったり来たりした後、最終的には順番が整い、そのままになります。

これは、位置の代わりにアイテムの「タイトル」を使用しているため、(サニタイズされた) デバッグ情報です。

[808:fb03] [ListOfItemsTVC performFetch] fetching Item with pedicate: parentList.guid ==    "DD1E1F25-BFC9-46B9-A637-109C0D6F0D1D" and sort: (title, ascending, compare:)
[808:fb03] fetched item: ccccc in array at index 0 
[808:fb03] fetched item: aaaaa in arrat at index 1
[808:fb03] fetched item: bbbbb in array at index 2

数回後戻りすると、aaaa、bbbb、ccccc という正しい順序に落ち着きます。ソートが壊れているか、FRCが壊れているようです。

4

3 に答える 3

0

CS193Pサンプルコードでも同じ並べ替えの問題が発生しました。私にとっての解決策は、親コンテキストも保存することでした(オブジェクトの作成/変更後):

NSError* err;
if ([txt.managedObjectContext save:&err]) {
    if ([txt.managedObjectContext.parentContext hasChanges]) {
        if ([txt.managedObjectContext.parentContext save:&err]) {
            NSLog(@"parent context save ok");
        } else {
            NSLog(@"can't save parent context; error: %@", err);
        }
    }
} else {
    NSLog(@"can't save text; error: %@", err);
}

、、およびUIDocumentを手動で設定した古いCore Dataテンプレートの代わりに新しいメソッドを使用すると、「バグ」が表示されるように見えます。NSManagedObjectContextNSPersistentStoreCoordinatorNSManagedObjectModel

于 2012-12-28T13:21:29.367 に答える
0

私はここに私の「解決策」を投稿していますが、それが単なるバグではなく、間違ったソートにつながるのがNSFetchedResultControllerの使用であることが判明した場合に備えて、しばらくの間これを回答として受け入れません。(質問の下のコメントを参照してください)

最後に、フェッチされたすべてのオブジェクトを取得し、目的の位置で1つを検索することで、不正行為を行います。これはすべての場合に機能するわけではありません。1つは整数で並べ替えているので簡単です。リストは最新であり(バックグラウンドで入力されていない)、小さいので大きなパフォーマンスはありません。すべてのオブジェクトを調べる際の問題。

具体的には、cellForRowAtIndexPathの一般的な行を次のように置き換えました。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    ....
    Item *item = [self.fetchedResultsController objectAtIndexPath:indexPath];
    ...

これとともに

{
    ...
    Item *item = [self _lookupItemAtIndexPath:indexPath];
    ...

ここに実装されています

- (Item *)_lookupItemAtIndexPath:(NSIndexPath *)indexPath
{
    // Since the FRC is sorted on position, _theoretically_ this next call should just
    // return the right item, but the damn sorting doesnt always work (esp not after
    // a new addition), so we check if it's the one we expect and look it up if not
    Item *item = [self.fetchedResultsController objectAtIndexPath:indexPath];

    if (item.position.integerValue != indexPath.row) {
        // the sorting failed! so do a lookup to find the item with the right position
        // (Note that its crucial that the positions remain contiguous and up-to-date)
        NSArray *items = self.fetchedResultsController.fetchedObjects;
        NSUInteger index = [items indexOfObjectPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) {
            if (((Item *)obj).position.integerValue == indexPath.row) {
                // found it - stop looking and return YES
                *stop = YES;
                return YES;
            }
            else {
                return NO;
            }
        }];

        NSAssert1(index != NSNotFound, @"Failed to find the item with the right position: %d", indexPath.row); 

        item = (Item *)[items objectAtIndex:index];

        // Temporary log for debugging: tells me how often the sort is actually failing
        NSLog(@"Warning: Item %@ was out of place, found it at %d instead of %d", item.title, index, indexPath.row);
    }
    return item;
}

最初に元のルックアップを試し、それが正しいかどうかをテストしているので、不要な場合は「遅い」ルックアップを実行しないことに注意してください。ほとんどの場合、それ_un_necessaryです-何かを追加した直後にのみ問題になるようです。(そこにあるNSLogメッセージからそれを知ることができます)

繰り返しになりますが、これは実際にはその種の失敗に対する答えではありません-私の特定のケースに対する私の回避策です-したがって、私はそれを答えとしてチェックしていません(まだ)

于 2012-11-15T20:01:22.350 に答える
0

OK、それで私はいくつかのツールを作りました。ダミープロジェクトで問題を再現しようとしましたが、できませんでした。これはおそらく、バグが Core Data ではなく、私のコードにあることを意味していると思います。見つけるのがあまりにも大変です。

そのため、更新が実際に保存されると問題が「消える」ことがわかっているので、強制的に保存する方法を調べました。Apple doc には、NSManagedObjectContext「ドキュメントが実行する他の重要な操作を回避する」ため、またはその親を保存しないでください。ただし、これは保存を強制する唯一の方法です (モックを保存するか updateChangeCount を使用するだけでは、実際には SQL が実行されません)。何が「回避」されているのかわからないため、zxcatからの応答に賛成票を投じたくありませんが、うまくいくようです。

更新:一晩中、私はそれについてさらに考え、「間違った」ことをしていても、まだバグがあるに違いないと判断しましたNSFetchedResultsController. これは、非論理的な状態になる可能性があるためです。の内容が の内容とfetchedObjects一致しませんsections。すべての場合において、fetchedObjects常に正しいです。私の場合やOPのように、sectionsメモリ内の状態ではなく、現在の永続化された状態を反映している場合があります。

于 2013-03-25T08:48:56.197 に答える