CollectionViewController.m line 439 __50-[CollectionViewController photoLibraryDidChange:]_block_invoke
CollectionViewController.m第439行__50- [CollectionViewController photoLibraryDidChange:] _ block_invoke
Fatal Exception: NSInternalInconsistencyException attempt to delete and reload the same index path ( {length = 2, path = 0 - 26007})
致命异常: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];
}
});
}
source: https://developer.apple.com/devcenter/download.action?path=/wwdc_2014/wwdc_2014_sample_code/exampleappusingphotosframework.zip
来源:https://developer.apple.com/devcenter/download.action?path = / wddc_2014 / wwwdc_2014_sample_code / examplesappusingphotosframework.zip
I can't replicate the issue. What could be the problem? Thanks a lot!
我不能复制这个问题。可能是什么问题呢?非常感谢!
17
I was able to reproduce this today. To do this you need to:
我今天能够重现这一点。为此,您需要:
I found an updated code that seems to work better to handle the updating behavior here: https://developer.apple.com/library/ios/documentation/Photos/Reference/PHPhotoLibraryChangeObserver_Protocol/
我发现一个更新的代码似乎更好地处理更新行为:https://developer.apple.com/library/ios/documentation/Photos/Reference/PHPhotoLibraryChangeObserver_Protocol/
But it still doesn't handle this situation nor when the indexes to be deleted are bigger (i.e. Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to delete item 9 from section 0 which only contains 9 items before the update'). I created this updated version of this code that deals with this better and hasn't crashed for me anymore so far.
但它仍然没有处理这种情况,也没有当要删除的索引更大时(即由于未捕获的异常'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 version:
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)
}
}
9
I just moved the reloadItemsAtIndexPaths
after the batch updates are completed to fix the crash of deleting and reloading at the same time.
我刚刚完成批量更新后移动了reloadItemsAtIndexPaths来修复同时删除和重新加载的崩溃。
From docs of changedIndexes
of PHFetchResultChangeDetails
:
来自PHFetchResultChangeDetails的changedIndexes的文档:
These indexes are relative to the original fetch result (the fetchResultBeforeChanges property) after you’ve applied the changes described by the removedIndexes and insertedIndexes properties; when updating your app’s interface, apply changes after removals and insertions and before moves.
在应用removedIndexes和insertedIndexes属性描述的更改后,这些索引相对于原始获取结果(fetchResultBeforeChanges属性);更新应用程序界面时,在删除和插入之后以及移动之前应用更改。
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]];
}
}
}
7
I implemented the code in batkryu's answer in Objective-C.
我在Objective-C中用batkryu的答案实现了代码。
- (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;
}
0
This is an improvement to @batkru's answer, which eliminates the need for the variable shouldReload
:
这是@ 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()
}
}
})
}
0
So I did well with @FernandoEscher's translation of @batkryu's solution, except in the situation where an iCloud Photo Library with tons of changes was recently re-conneted. In this situation the collection becomes totally un-responsive and can crash. The core problem is that photoLibraryDidChange will get called again before the performBatchUpdates completion fires. The call to performBatchUpdates before a performBatchUpdates finishes seems to kill performance. I suspect that the crash happens because assetsFetchResults gets modified while the animation is running for its previous value.
所以我在@ FernandoEscher的@ batkryu解决方案的翻译中做得很好,除了最近重新连接了大量变化的iCloud Photo Library之外。在这种情况下,集合变得完全无响应并且可能崩溃。核心问题是在performBatchUpdates完成触发之前将再次调用photoLibraryDidChange。在performBatchUpdates完成之前调用performBatchUpdates似乎会破坏性能。我怀疑崩溃的发生是因为在动画运行前一个值时,assetsFetchResults被修改了。
Sooooo, here's what I did:
Sooooo,这就是我做的:
elsewhere in the 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;
}