You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 07:59:25