CRASH пытается удалить и перезагрузить один и тот же путь индекса

CollectionViewController.m строка 439 __50- [CollectionViewController photoLibraryDidChange:] _ block_invoke

Фатальное исключение: исключение NSInternalInconsistencyException попытайтесь удалить и перезагрузить один и тот же путь индекса ({length = 2, path = 0 - 26007})

- (void)photoLibraryDidChange:(PHChange *)changeInstance
{
    // Call might come on any background queue. Re-dispatch to the main queue to handle it.
    dispatch_async(dispatch_get_main_queue(), ^{

        // check if there are changes to the assets (insertions, deletions, updates)
        PHFetchResultChangeDetails *collectionChanges = [changeInstance changeDetailsForFetchResult:self.assetsFetchResults];
        if (collectionChanges) {

            // get the new fetch result
            self.assetsFetchResults = [collectionChanges fetchResultAfterChanges];

            UICollectionView *collectionView = self.collectionView;

            if (![collectionChanges hasIncrementalChanges] || [collectionChanges hasMoves]) {
                // we need to reload all if the incremental diffs are not available
                [collectionView reloadData];

            } else {
                // if we have incremental diffs, tell the collection view to animate insertions and deletions
                [collectionView performBatchUpdates:^{
                    NSIndexSet *removedIndexes = [collectionChanges removedIndexes];
                    if ([removedIndexes count]) {
                        [collectionView deleteItemsAtIndexPaths:[removedIndexes aapl_indexPathsFromIndexesWithSection:0]];
                    }
                    NSIndexSet *insertedIndexes = [collectionChanges insertedIndexes];
                    if ([insertedIndexes count]) {
                        [collectionView insertItemsAtIndexPaths:[insertedIndexes aapl_indexPathsFromIndexesWithSection:0]];
                    }
                    NSIndexSet *changedIndexes = [collectionChanges changedIndexes];
                    if ([changedIndexes count]) {
                        [collectionView reloadItemsAtIndexPaths:[changedIndexes aapl_indexPathsFromIndexesWithSection:0]];
                    }
                } completion:NULL];
            }

            [self resetCachedAssets];
        }
    });
}

источник: https://developer.apple.com/devcenter/download.action?path=/wwdc_2014/wwdc_2014_sample_code/exampleappusingphotosframework.zip

Я не могу воспроизвести проблему. В чем может быть проблема? Большое спасибо!

Ответ 1

Я смог воспроизвести это сегодня. Для этого вам необходимо:

  • Откройте приложение, которое прослушивает изменения.
  • Откройте приложение "Фотографии", сохраните набор фотографий в библиотеке фотографий из общего альбома iCloud.
  • Перейдите в приложение для фотографий, удалите некоторые из этих фотографий.
  • Перейдите к общему альбому iCloud и снова сохраните некоторые из фотографий, которые вы удалили. Вы увидите это условие.

Я нашел обновленный код, который, кажется, работает лучше, чтобы справиться с поведением обновления здесь: https://developer.apple.com/library/ios/documentation/Photos/Reference/PHPhotoLibraryChangeObserver_Protocol/

Но он по-прежнему не справляется с этой ситуацией или когда индексы, подлежащие удалению, больше (т.е. завершение приложения из-за неотображенного исключения "NSInternalInconsistencyException", причина: "попытаться удалить элемент 9 из раздела 0, который содержит только 9 элементов обновление'). Я создал эту обновленную версию этого кода, которая справляется с этим лучше и до сих пор не разбилась для меня.

func photoLibraryDidChange(changeInfo: PHChange!) {

    // Photos may call this method on a background queue;
    // switch to the main queue to update the UI.
    dispatch_async(dispatch_get_main_queue()) {


        // Check for changes to the list of assets (insertions, deletions, moves, or updates).
        if let collectionChanges = changeInfo.changeDetailsForFetchResult(self.assetsFetchResult) {

            // Get the new fetch result for future change tracking.
            self.assetsFetchResult = collectionChanges.fetchResultAfterChanges

            if collectionChanges.hasIncrementalChanges {

                // Get the changes as lists of index paths for updating the UI.
                var removedPaths: [NSIndexPath]?
                var insertedPaths: [NSIndexPath]?
                var changedPaths: [NSIndexPath]?
                if let removed = collectionChanges.removedIndexes {
                    removedPaths = self.indexPathsFromIndexSetWithSection(removed,section: 0)
                }
                if let inserted = collectionChanges.insertedIndexes {
                    insertedPaths = self.indexPathsFromIndexSetWithSection(inserted,section: 0)
                }
                if let changed = collectionChanges.changedIndexes {
                    changedPaths = self.indexPathsFromIndexSetWithSection(changed,section: 0)
                }
                var shouldReload = false
                if changedPaths != nil && removedPaths != nil{
                    for changedPath in changedPaths!{
                        if contains(removedPaths!,changedPath){
                            shouldReload = true
                            break
                        }
                    }

                }

                if removedPaths?.last?.item >= self.assetsFetchResult.count{
                    shouldReload = true
                }

                if shouldReload{
                    self.collectionView.reloadData()
                }else{
                    // Tell the collection view to animate insertions/deletions/moves
                    // and to refresh any cells that have changed content.
                    self.collectionView.performBatchUpdates(
                        {
                            if let theRemovedPaths = removedPaths {
                                self.collectionView.deleteItemsAtIndexPaths(theRemovedPaths)
                            }
                            if let theInsertedPaths = insertedPaths {
                                self.collectionView.insertItemsAtIndexPaths(theInsertedPaths)
                            }
                            if let theChangedPaths = changedPaths{
                                self.collectionView.reloadItemsAtIndexPaths(theChangedPaths)
                            }
                            if (collectionChanges.hasMoves) {
                                collectionChanges.enumerateMovesWithBlock() { fromIndex, toIndex in
                                    let fromIndexPath = NSIndexPath(forItem: fromIndex, inSection: 0)
                                    let toIndexPath = NSIndexPath(forItem: toIndex, inSection: 0)
                                    self.collectionView.moveItemAtIndexPath(fromIndexPath, toIndexPath: toIndexPath)
                                }
                            }
                        }, completion: nil)

                }

            } else {
                // Detailed change information is not available;
                // repopulate the UI from the current fetch result.
                self.collectionView.reloadData()
            }
        }
    }
}

func indexPathsFromIndexSetWithSection(indexSet:NSIndexSet?,section:Int) -> [NSIndexPath]?{
    if indexSet == nil{
        return nil
    }
    var indexPaths:[NSIndexPath] = []

    indexSet?.enumerateIndexesUsingBlock { (index, Bool) -> Void in
        indexPaths.append(NSIndexPath(forItem: index, inSection: section))
    }
    return indexPaths

}

версия Swift 3/iOS 10:

func photoLibraryDidChange(_ changeInstance: PHChange) {
    guard let collectionView = self.collectionView else {
        return
    }

    // Photos may call this method on a background queue;
    // switch to the main queue to update the UI.
    DispatchQueue.main.async {
        guard let fetchResults = self.fetchResults else {
            collectionView.reloadData()
            return
        }

        // Check for changes to the list of assets (insertions, deletions, moves, or updates).
        if let collectionChanges = changeInstance.changeDetails(for: fetchResults) {
            // Get the new fetch result for future change tracking.
            self.fetchResults = collectionChanges.fetchResultAfterChanges

            if collectionChanges.hasIncrementalChanges {
                // Get the changes as lists of index paths for updating the UI.
                var removedPaths: [IndexPath]?
                var insertedPaths: [IndexPath]?
                var changedPaths: [IndexPath]?
                if let removed = collectionChanges.removedIndexes {
                    removedPaths = self.indexPaths(from: removed, section: 0)
                }
                if let inserted = collectionChanges.insertedIndexes {
                    insertedPaths = self.indexPaths(from:inserted, section: 0)
                }
                if let changed = collectionChanges.changedIndexes {
                    changedPaths = self.indexPaths(from: changed, section: 0)
                }
                var shouldReload = false
                if let removedPaths = removedPaths, let changedPaths = changedPaths {
                    for changedPath in changedPaths {
                        if removedPaths.contains(changedPath) {
                            shouldReload = true
                            break
                        }
                    }
                }

                if let item = removedPaths?.last?.item {
                    if item >= fetchResults.count {
                        shouldReload = true
                    }
                }

                if shouldReload {
                    collectionView.reloadData()
                } else {
                    // Tell the collection view to animate insertions/deletions/moves
                    // and to refresh any cells that have changed content.
                    collectionView.performBatchUpdates({
                        if let theRemovedPaths = removedPaths {
                            collectionView.deleteItems(at: theRemovedPaths)
                        }
                        if let theInsertedPaths = insertedPaths {
                            collectionView.insertItems(at: theInsertedPaths)
                        }
                        if let theChangedPaths = changedPaths {
                            collectionView.reloadItems(at: theChangedPaths)
                        }

                        collectionChanges.enumerateMoves { fromIndex, toIndex in
                            collectionView.moveItem(at: IndexPath(item: fromIndex, section: 0),
                                                    to: IndexPath(item: toIndex, section: 0))
                        }
                    })
                }
            } else {
                // Detailed change information is not available;
                // repopulate the UI from the current fetch result.
                collectionView.reloadData()
            }
        }
    }
}

func indexPaths(from indexSet: IndexSet?, section: Int) -> [IndexPath]? {
    guard let set = indexSet else {
        return nil
    }

    return set.map { (index) -> IndexPath in
        return IndexPath(item: index, section: section)
    }
}

Ответ 2

Я реализовал код batkryu answer в Objective-C.

- (void)photoLibraryDidChange:(PHChange *)changeInstance {

    dispatch_async(dispatch_get_main_queue(), ^{

        PHFetchResultChangeDetails *collectionChanges = [changeInstance changeDetailsForFetchResult:self.assetsFetchResults];
        if (collectionChanges) {

            self.assetsFetchResults = [collectionChanges fetchResultAfterChanges];

            UICollectionView *collectionView = self.collectionView;
            NSArray *removedPaths;
            NSArray *insertedPaths;
            NSArray *changedPaths;

            if ([collectionChanges hasIncrementalChanges]) {
                NSIndexSet *removedIndexes = [collectionChanges removedIndexes];
                removedPaths = [self indexPathsFromIndexSet:removedIndexes withSection:0];

                NSIndexSet *insertedIndexes = [collectionChanges insertedIndexes];
                insertedPaths = [self indexPathsFromIndexSet:insertedIndexes withSection:0];

                NSIndexSet *changedIndexes = [collectionChanges changedIndexes];
                changedPaths = [self indexPathsFromIndexSet:changedIndexes withSection:0];

                BOOL shouldReload = NO;

                if (changedPaths != nil && removedPaths != nil) {
                    for (NSIndexPath *changedPath in changedPaths) {
                        if ([removedPaths containsObject:changedPath]) {
                            shouldReload = YES;
                            break;
                        }
                    }
                }

                if (removedPaths.lastObject && ((NSIndexPath *)removedPaths.lastObject).item >= self.assetsFetchResults.count) {
                    shouldReload = YES;
                }

                if (shouldReload) {
                    [collectionView reloadData];

                } else {
                    [collectionView performBatchUpdates:^{
                        if (removedPaths) {
                            [collectionView deleteItemsAtIndexPaths:removedPaths];
                        }

                        if (insertedPaths) {
                            [collectionView insertItemsAtIndexPaths:insertedPaths];
                        }

                        if (changedPaths) {
                            [collectionView reloadItemsAtIndexPaths:changedPaths];
                        }

                        if ([collectionChanges hasMoves]) {
                            [collectionChanges enumerateMovesWithBlock:^(NSUInteger fromIndex, NSUInteger toIndex) {
                                NSIndexPath *fromIndexPath = [NSIndexPath indexPathForItem:fromIndex inSection:0];
                                NSIndexPath *toIndexPath = [NSIndexPath indexPathForItem:toIndex inSection:0];
                                [collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:toIndexPath];
                            }];
                        }

                    } completion:NULL];
                }

                [self resetCachedAssets];
            } else {
                [collectionView reloadData];
            }
        }
    });
}

- (NSArray *)indexPathsFromIndexSet:(NSIndexSet *)indexSet withSection:(int)section {
    if (indexSet == nil) {
        return nil;
    }
    NSMutableArray *indexPaths = [[NSMutableArray alloc] init];

    [indexSet enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
        [indexPaths addObject:[NSIndexPath indexPathForItem:idx inSection:section]];
    }];

    return indexPaths;
}

Ответ 3

Я только что переместил reloadItemsAtIndexPaths после завершения пакетных обновлений для исправления сбоя при удалении и перезагрузке в одно и то же время.

Из документов changedIndexes of PHFetchResultChangeDetails:

Эти индексы относятся к исходному результату выборки ( fetchResultBeforeChanges) после того, как вы применили изменения описывается свойствами removeIndexes и insertIndexes; когда обновление интерфейса приложений, применить изменения после удаления и вставки и перед перемещениями.

PHFetchResultChangeDetails *collectionChanges = [changeInstance changeDetailsForFetchResult:self.assetsFetchResults];
[collectionView performBatchUpdates:^{ 
        NSIndexSet *removedIndexes = [collectionChanges removedIndexes];
        if ([removedIndexes count]) {
            [collectionView deleteItemsAtIndexPaths:[self indexPathsFromIndexes:removedIndexes withSection:0]];
        }
        NSIndexSet *insertedIndexes = [collectionChanges insertedIndexes];
        if ([insertedIndexes count]) {
            [collectionView insertItemsAtIndexPaths:[self indexPathsFromIndexes:insertedIndexes withSection:0]];
        }
    } completion:^(BOOL finished) {
        if (finished) {
            // Puting this after removes and inserts indexes fixes a crash of deleting and reloading at the same time.
            // From docs: When updating your app’s interface, apply changes after removals and insertions and before moves.
            NSIndexSet *changedIndexes = [collectionChanges changedIndexes];
            if ([changedIndexes count]) {
                [collectionView reloadItemsAtIndexPaths:[self indexPathsFromIndexes:changedIndexes withSection:0]];
            }
       }
    }

Ответ 4

Это улучшение для ответа @batkru, что устраняет необходимость в переменной shouldReload:

func photoLibraryDidChange(changeInstance: PHChange) {
    dispatch_async(dispatch_get_main_queue(), {
        let changeDetails = changeInstance.changeDetailsForFetchResult(self.assetsFetchResult)

        if let details = changeDetails {
            self.assetsFetchResult = details.fetchResultAfterChanges

            if details.hasIncrementalChanges {
                var removedIndexes: [NSIndexPath]?
                var insertedIndexes: [NSIndexPath]?
                var changedIndexes: [NSIndexPath]?

                if let removed = details.removedIndexes {
                    removedIndexes = createIndexPathsFromIndices(removed)
                }
                if let inserted = details.insertedIndexes {
                    insertedIndexes = createIndexPathsFromIndices(inserted)
                }
                if let changed = details.changedIndexes {
                    changedIndexes = createIndexPathsFromIndices(changed)
                }

                if removedIndexes != nil && changedIndexes != nil {
                    for removedIndex in removedIndexes! {
                        let indexOfAppearanceOfRemovedIndexInChangedIndexes = find(changedIndexes!, removedIndex)
                        if let index = indexOfAppearanceOfRemovedIndexInChangedIndexes {
                            changedIndexes!.removeAtIndex(index)
                        }
                    }
                }

                self.collectionView?.performBatchUpdates({
                    if let removed = removedIndexes {
                        self.collectionView?.deleteItemsAtIndexPaths(removed)
                    }
                    if let inserted = insertedIndexes {
                        self.collectionView?.insertItemsAtIndexPaths(inserted)
                    }
                    if let changed = changedIndexes {
                        self.collectionView?.reloadItemsAtIndexPaths(changed)
                    }
                    if details.hasMoves {
                        changeDetails!.enumerateMovesWithBlock({ fromIndex, toIndex in
                            self.collectionView?.moveItemAtIndexPath(NSIndexPath(forItem: fromIndex, inSection: 0), toIndexPath: NSIndexPath(forItem: toIndex, inSection: 0))
                        })
                    }
                }, completion: nil)
            } else {
                self.collectionView?.reloadData()
            }
        }
    })
}

Ответ 5

Итак, я преуспел в переводе @FernandoEscher решения @batkryu, за исключением ситуации, когда недавно была повторно подключена iCDoud Photo Library с тоннами изменений. В этой ситуации коллекция становится полностью невосприимчивой и может потерпеть крах. Основная проблема заключается в том, что photoLibraryDidChange будет вызван снова до завершения выполнения executeBatchUpdates. Вызов executeBatchUpdates перед завершением executeBatchUpdates, похоже, приводит к снижению производительности. Я подозреваю, что авария происходит из-за того, что assetsFetchResults изменяется, пока анимация работает для своего предыдущего значения.

Sooooo, вот что я сделал:

в другом месте в init...

self.phPhotoLibChageMutex = dispatch_semaphore_create(1);

_

- (void)photoLibraryDidChange:(PHChange *)changeInstance {
    dispatch_semaphore_wait(self.phPhotoLibChageMutex, DISPATCH_TIME_FOREVER);

    dispatch_async(dispatch_get_main_queue(), ^{

        PHFetchResultChangeDetails *collectionChanges = [changeInstance changeDetailsForFetchResult:self.assetsFetchResults];
        if (collectionChanges) {

            self.assetsFetchResults = [collectionChanges fetchResultAfterChanges];

            UICollectionView *collectionView = self.collectionView;
            NSArray *removedPaths;
            NSArray *insertedPaths;
            NSArray *changedPaths;

            if ([collectionChanges hasIncrementalChanges]) {
                NSIndexSet *removedIndexes = [collectionChanges removedIndexes];
                removedPaths = [self indexPathsFromIndexSet:removedIndexes withSection:0];

                NSIndexSet *insertedIndexes = [collectionChanges insertedIndexes];
                insertedPaths = [self indexPathsFromIndexSet:insertedIndexes withSection:0];

                NSIndexSet *changedIndexes = [collectionChanges changedIndexes];
                changedPaths = [self indexPathsFromIndexSet:changedIndexes withSection:0];

                BOOL shouldReload = NO;

                if (changedPaths != nil && removedPaths != nil) {
                    for (NSIndexPath *changedPath in changedPaths) {
                        if ([removedPaths containsObject:changedPath]) {
                            shouldReload = YES;
                            break;
                        }
                    }
                }

                if (removedPaths.lastObject && ((NSIndexPath *)removedPaths.lastObject).item >= self.assetsFetchResults.count) {
                    shouldReload = YES;
                }

                if (shouldReload) {
                    [collectionView reloadData];
                    [self fixupSelection];
                    dispatch_semaphore_signal(self.phPhotoLibChageMutex);
                } else {
                    [collectionView performBatchUpdates:^{
                        if (removedPaths) {
                            [collectionView deleteItemsAtIndexPaths:removedPaths];
                        }

                        if (insertedPaths) {
                            [collectionView insertItemsAtIndexPaths:insertedPaths];
                        }

                        if (changedPaths) {
                            [collectionView reloadItemsAtIndexPaths:changedPaths];
                        }

                        if ([collectionChanges hasMoves]) {
                            [collectionChanges enumerateMovesWithBlock:^(NSUInteger fromIndex, NSUInteger toIndex) {
                                NSIndexPath *fromIndexPath = [NSIndexPath indexPathForItem:fromIndex inSection:0];
                                NSIndexPath *toIndexPath = [NSIndexPath indexPathForItem:toIndex inSection:0];
                                [collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:toIndexPath];
                            }];
                        }

                    } completion:^(BOOL finished) {
                        [self fixupSelection];
                        dispatch_semaphore_signal(self.phPhotoLibChageMutex);
                    }];
                }

                [self resetCachedAssets];
            } else {
                [collectionView reloadData];
                [self fixupSelection];
                dispatch_semaphore_signal(self.phPhotoLibChageMutex);
            }
        }else{
            dispatch_semaphore_signal(self.phPhotoLibChageMutex);
        }
    });
}

- (NSArray *)indexPathsFromIndexSet:(NSIndexSet *)indexSet withSection:(int)section {
    if (indexSet == nil) {
        return nil;
    }
    NSMutableArray *indexPaths = [[NSMutableArray alloc] init];

    [indexSet enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
        [indexPaths addObject:[NSIndexPath indexPathForItem:idx inSection:section]];
    }];

    return indexPaths;
}