From 1a1ac6ad5b71cc3c89c944ed0f1240768313c242 Mon Sep 17 00:00:00 2001 From: Iiro Vidberg Date: Fri, 13 Feb 2026 16:22:20 -0500 Subject: [PATCH] fix: Make the photo library change observer more resilient --- .../Extensions/IndexSet+Extensions.swift | 4 +- .../Gallery/YPLibrary+LibraryChange.swift | 40 ++++++++++++++++--- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/Source/Helpers/Extensions/IndexSet+Extensions.swift b/Source/Helpers/Extensions/IndexSet+Extensions.swift index df54fa7a..f15949ad 100644 --- a/Source/Helpers/Extensions/IndexSet+Extensions.swift +++ b/Source/Helpers/Extensions/IndexSet+Extensions.swift @@ -9,11 +9,11 @@ import Foundation internal extension IndexSet { - func aapl_indexPathsFromIndexesWithSection(_ section: Int) -> [IndexPath] { + func aapl_indexPathsFromIndexesWithSection(_ section: Int, offset: Int = 0) -> [IndexPath] { var indexPaths: [IndexPath] = [] indexPaths.reserveCapacity(count) (self as NSIndexSet).enumerate({idx, _ in - indexPaths.append(IndexPath(item: idx, section: section)) + indexPaths.append(IndexPath(item: idx + offset, section: section)) }) return indexPaths } diff --git a/Source/Pages/Gallery/YPLibrary+LibraryChange.swift b/Source/Pages/Gallery/YPLibrary+LibraryChange.swift index 77724c33..2d795ab6 100644 --- a/Source/Pages/Gallery/YPLibrary+LibraryChange.swift +++ b/Source/Pages/Gallery/YPLibrary+LibraryChange.swift @@ -23,29 +23,57 @@ extension YPLibraryVC: PHPhotoLibraryChangeObserver { DispatchQueue.main.async { let collectionView = self.v.collectionView - self.mediaManager.fetchResult = collectionChanges.fetchResultAfterChanges + + // Guard against updates when the view isn't in the hierarchy + guard collectionView.window != nil else { + self.mediaManager.fetchResult = collectionChanges.fetchResultAfterChanges + collectionView.reloadData() + self.updateAssetSelection() + self.mediaManager.resetCachedAssets() + return + } + if !collectionChanges.hasIncrementalChanges || collectionChanges.hasMoves { + self.mediaManager.fetchResult = collectionChanges.fetchResultAfterChanges collectionView.reloadData() } else { + // Validate that the current data source count matches what PHChange expects + // before attempting incremental updates + let expectedOldCount = collectionChanges.fetchResultBeforeChanges.count + let cameraButtonOffset = YPConfig.library.showGalleryCameraButton ? 1 : 0 + let actualOldCount = collectionView.numberOfItems(inSection: 0) - cameraButtonOffset + + guard expectedOldCount == actualOldCount else { + ypLog("Data source count mismatch (expected: \(expectedOldCount), actual: \(actualOldCount)). Falling back to reloadData.") + self.mediaManager.fetchResult = collectionChanges.fetchResultAfterChanges + collectionView.reloadData() + self.updateAssetSelection() + self.mediaManager.resetCachedAssets() + return + } + collectionView.performBatchUpdates({ + // Update fetchResult inside performBatchUpdates so that + // UICollectionView sees the old count at the start and the + // new count at the end, matching the applied deletes/inserts. + self.mediaManager.fetchResult = collectionChanges.fetchResultAfterChanges + if let removedIndexes = collectionChanges.removedIndexes, removedIndexes.count != 0 { - collectionView.deleteItems(at: removedIndexes.aapl_indexPathsFromIndexesWithSection(0)) + collectionView.deleteItems(at: removedIndexes.aapl_indexPathsFromIndexesWithSection(0, offset: cameraButtonOffset)) } if let insertedIndexes = collectionChanges.insertedIndexes, insertedIndexes.count != 0 { - collectionView.insertItems(at: insertedIndexes.aapl_indexPathsFromIndexesWithSection(0)) + collectionView.insertItems(at: insertedIndexes.aapl_indexPathsFromIndexesWithSection(0, offset: cameraButtonOffset)) } }, completion: { finished in guard finished else { return } guard let changedIndexes = collectionChanges.changedIndexes, changedIndexes.count != 0 else { - ypLog("No changes detected") - collectionView.reloadData() // If we failed to detect changes, we'll reload everything just in case return } - collectionView.reloadItems(at: changedIndexes.aapl_indexPathsFromIndexesWithSection(0)) + collectionView.reloadItems(at: changedIndexes.aapl_indexPathsFromIndexesWithSection(0, offset: cameraButtonOffset)) }) }