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];
}
});
}
我无法复制这个问题。可能是什么问题呢?非常感谢!
答案 0 :(得分:18)
我今天能够重现这一点。要做到这一点,你需要:
我发现一个更新的代码似乎更适合处理更新行为: https://developer.apple.com/library/ios/documentation/Photos/Reference/PHPhotoLibraryChangeObserver_Protocol/
但是它仍然没有处理这种情况,也没有要删除的索引更大(即由于未捕获的异常终止应用程序' NSInternalInconsistencyException',原因:'尝试删除项目来自第0部分的9,在更新之前仅包含9个项目#39;)。我创建了这段代码的更新版本,这个版本更好地解决了这个问题,到目前为止还没有为我崩溃。
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)
}
}
答案 1 :(得分:11)
我在批量更新完成后移动了reloadItemsAtIndexPaths
,以便同时修复删除和重新加载的崩溃。
来自changedIndexes
的{{1}}的文档:
这些索引与原始获取结果相关( 应用更改后,fetchResultBeforeChanges属性) 由removedIndexes和insertedIndexes属性描述;什么时候 更新应用的界面,删除后应用更改 插入和移动之前。
PHFetchResultChangeDetails
答案 2 :(得分:7)
我在Objective-C中的batkryu's answer中实现了代码。
- (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 :(得分:3)
最后阅读文档(不是从developer.apple.com,而是在Xcode中阅读),应用增量更改的正确方法是:
collectionView.performBatchUpdates({
// removedIndexes
// insertedIndexes
}, completion: {
// changedIndexes (using collectionView.cellForItem(at: index))
collectionView.performBatchUpdates({
// enumerateMoves
})
})
在实施这种方法之前,我们在移动物品时会随机崩溃。
为什么这是正确的?
“ 已删除索引”和“ 插入索引”排在第一位,因为该索引是相对于原始提取结果而言的
“ removedIndexes ”-“ 索引相对于原始提取结果(fetchResultBeforeChanges属性);在更新应用界面时,请在插入,更改和移动之前应用删除。” “ insertedIndexes”-“索引是在应用了removeIndexes属性描述的更改之后相对于原始获取结果(fetchResultBeforeChanges属性)的;在更新应用程序的界面时,请在删除之后以及更改和移动之前应用插入。“
“ changedIndexes ”不能用“删除” /“插入”处理,因为它们描述了插入/删除后的项目
“ changedIndexes ”-“ 这些索引是相对于原始的获取结果(fetchResultBeforeChanges属性)的,在您应用了removeIndexes和insertedIndexes属性所描述的更改后;在更新您的应用程序的界面,请在删除和插入后以及移动之前应用更改。
警告 在批处理更新中,不要将changesIndexes直接映射到UICollectionView项索引。在performBatchUpdates(_:completion :)之后,使用这些索引重新配置相应的单元格。 UICollectionView和UITableView期望changedIndexes处于before状态,而PhotoKit将它们置于after状态,如果您的应用在更改的同时执行插入和删除操作,则会导致崩溃。“
“ enumerateMoves ”是我们应该做的最后一件事。
“ enumerateMoves ”-“ 在应用了removeedIndexes,insertIndexes和changedIndexes属性描述的更改后,处理程序块中的toIndex参数相对于获取结果的状态因此,如果您使用此方法更新显示获取结果内容的集合视图或类似的用户界面,请在处理移动之前更新您的UI以反映插入,删除和更改。“
答案 4 :(得分:0)
这是@ 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 :(得分:0)
所以我很好地使用了@ FernandoEscher对@ batkryu解决方案的翻译,除了最近重新连接了大量变化的iCloud Photo Library之外。在这种情况下,集合变得完全无响应并且可能崩溃。核心问题是在performBatchUpdates完成触发之前将再次调用photoLibraryDidChange。在performBatchUpdates完成之前调用performBatchUpdates似乎会破坏性能。我怀疑崩溃的发生是因为在动画运行前一个值时,assetsFetchResults被修改了。
Sooooo,这就是我所做的:
初始化中的其他地方......
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;
}