Swift自定义照片选择器:UICollectionView缩略图滚动混乱问题求解
这个问题在基于UICollectionView/UITableView的媒体选择器里太常见了!核心原因就是单元格复用+异步加载的竞态条件——当你快速滚动时,之前给某个单元格发起的缩略图请求还没完成,这个单元格已经被复用去展示另一个asset了,等旧请求完成后,就把错误的图片塞到了当前单元格里。下面给你几个层层递进的解决方案,从基础到优化:
1. 基础方案:复用单元格时取消旧请求
首先要给每个单元格绑定当前的加载请求,在单元格被复用时立刻取消未完成的请求,同时清空图片(或设置占位图),避免旧内容残留。
class PhotoCell: UICollectionViewCell { @IBOutlet weak var imageView: UIImageView! private var currentRequestID: PHImageRequestID? override func prepareForReuse() { super.prepareForReuse() // 取消当前未完成的请求 if let requestID = currentRequestID { PHImageManager.default().cancelImageRequest(requestID) } currentRequestID = nil // 清空图片,替换成你的占位图 imageView.image = UIImage(named: "photo_placeholder") } func configure(with asset: PHAsset, targetSize: CGSize) { currentRequestID = PHImageManager.default().requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: nil) { [weak self] image, info in guard let self = self else { return } // 检查请求是否被取消,避免无效回调 if let isCancelled = info?[PHImageResultIsCancelledKey] as? Bool, isCancelled { return } self.imageView.image = image ?? UIImage(named: "photo_placeholder") self.currentRequestID = nil } } }
2. 优化方案:加一层内存缓存避免重复生成缩略图
PHImageManager自带缓存,但自己加一层NSCache可以更精准地控制同尺寸同asset的缩略图复用,减少不必要的异步请求,从根源降低错乱概率。
// 全局缓存单例 class PhotoThumbnailCache { static let shared = PhotoThumbnailCache() private let cache = NSCache<NSString, UIImage>() func getThumbnail(for assetID: String, size: CGSize) -> UIImage? { let key = "\(assetID)_\(size.width)x\(size.height)" as NSString return cache.object(forKey: key) } func setThumbnail(_ image: UIImage, for assetID: String, size: CGSize) { let key = "\(assetID)_\(size.width)x\(size.height)" as NSString cache.setObject(image, forKey: key) } } // 更新PhotoCell的configure方法 func configure(with asset: PHAsset, targetSize: CGSize) { let assetID = asset.localIdentifier // 先查缓存,命中直接显示 if let cachedImage = PhotoThumbnailCache.shared.getThumbnail(for: assetID, size: targetSize) { imageView.image = cachedImage return } currentRequestID = PHImageManager.default().requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: nil) { [weak self] image, info in guard let self = self, let image = image else { return } if let isCancelled = info?[PHImageResultIsCancelledKey] as? Bool, isCancelled { return } // 缓存新生成的缩略图 PhotoThumbnailCache.shared.setThumbnail(image, for: assetID, size: targetSize) self.imageView.image = image self.currentRequestID = nil } }
3. 进阶方案:用assetID标记确保回调正确性
有时候即使取消了请求,还是可能遇到请求已经进入回调阶段但还没执行的情况。这时候给单元格加一个currentAssetID标记,回调时检查当前单元格的asset是否和请求的一致,不一致就跳过更新。
class PhotoCell: UICollectionViewCell { @IBOutlet weak var imageView: UIImageView! private var currentRequestID: PHImageRequestID? private var currentAssetID: String? // 新增asset标记 override func prepareForReuse() { super.prepareForReuse() if let requestID = currentRequestID { PHImageManager.default().cancelImageRequest(requestID) } currentRequestID = nil currentAssetID = nil imageView.image = UIImage(named: "photo_placeholder") } func configure(with asset: PHAsset, targetSize: CGSize) { let assetID = asset.localIdentifier currentAssetID = assetID // 绑定当前asset if let cachedImage = PhotoThumbnailCache.shared.getThumbnail(for: assetID, size: targetSize) { imageView.image = cachedImage return } currentRequestID = PHImageManager.default().requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: nil) { [weak self] image, info in guard let self = self, let image = image else { return } // 关键检查:当前单元格是否还在展示这个asset guard self.currentAssetID == assetID else { return } if let isCancelled = info?[PHImageResultIsCancelledKey] as? Bool, isCancelled { return } PhotoThumbnailCache.shared.setThumbnail(image, for: assetID, size: targetSize) self.imageView.image = image self.currentRequestID = nil } } }
终极优化:用
PHCachingImageManager批量预加载 苹果提供的PHCachingImageManager可以提前预加载可见区域附近的asset缩略图,滚动时几乎不会出现加载延迟,从根源上减少错乱的可能。
// 在你的照片选择器ViewController中 class PhotoPickerVC: UICollectionViewController { private let cachingImageManager = PHCachingImageManager() private var assets: [PHAsset] = [] private let thumbnailSize = CGSize(width: 150, height: 150) // 根据你的单元格尺寸调整 override func viewDidLoad() { super.viewDidLoad() cachingImageManager.allowsCachingHighQualityImages = false // 优先缓存低质量缩略图,加快加载 } // 滚动时更新预加载范围 override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { updateCachedAssets() } override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { if !decelerate { updateCachedAssets() } } override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { updateCachedAssets() } private func updateCachedAssets() { guard let visibleIndexPaths = collectionView.indexPathsForVisibleItems else { return } let firstVisibleIndex = visibleIndexPaths.first!.item let lastVisibleIndex = visibleIndexPaths.last!.item // 预加载可见区域前后各10个item,可根据需求调整 let preloadStart = max(0, firstVisibleIndex - 10) let preloadEnd = min(assets.count - 1, lastVisibleIndex + 10) let preloadAssets = assets[preloadStart...preloadEnd] // 开始缓存预加载范围的asset cachingImageManager.startCachingImages(for: Array(preloadAssets), targetSize: thumbnailSize, contentMode: .aspectFill, options: nil) // 停止缓存不在预加载范围的asset,节省内存 let uncachedAssets = assets.filter { !preloadAssets.contains($0) } cachingImageManager.stopCachingImages(for: uncachedAssets, targetSize: thumbnailSize, contentMode: .aspectFill, options: nil) } // 单元格配置时使用cachingImageManager override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PhotoCell", for: indexPath) as! PhotoCell let asset = assets[indexPath.item] cell.configure(with: asset, targetSize: thumbnailSize, imageManager: cachingImageManager) return cell } } // 更新PhotoCell的configure方法,支持传入cachingImageManager func configure(with asset: PHAsset, targetSize: CGSize, imageManager: PHCachingImageManager) { let assetID = asset.localIdentifier currentAssetID = assetID if let cachedImage = PhotoThumbnailCache.shared.getThumbnail(for: assetID, size: targetSize) { imageView.image = cachedImage return } currentRequestID = imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: nil) { [weak self] image, info in guard let self = self, let image = image else { return } guard self.currentAssetID == assetID else { return } if let isCancelled = info?[PHImageResultIsCancelledKey] as? Bool, isCancelled { return } PhotoThumbnailCache.shared.setThumbnail(image, for: assetID, size: targetSize) self.imageView.image = image self.currentRequestID = nil } }
这些方案可以组合使用,基础的“取消请求+assetID检查”就能解决绝大多数错乱问题,加上缓存和预加载能让选择器的滚动体验更加流畅。
内容的提问来源于stack exchange,提问作者K.Wu
相关产品推荐
相关产品推荐

