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

解决PHPickerViewControllerDelegate的NSCocoaErrorDomain与图片显示缺失问题

图片编辑App PhotoUI加载问题修复

问题描述

开发图片编辑App时,通过PhotoUI从相册选择多张图片/视频并在UICollectionView展示,遇到以下问题:

  • 选择6张图片时,UICollectionView仅显示5张
  • 日志抛出Error Domain=NSCocoaErrorDomain Code=260错误,提示目标文件不存在

错误日志

Filename: IMG_0001
File Address: <PUPhotosFileProviderItemProvider: 0x6000038b5c00> {types = (
"public.jpeg"
)}
2023-08-21 11:18:11.204644+0600 PhotoCollection[66458:2314832] [claims] Upload preparation for claim 1BD1CC9C-9E04-4269-ABC8-5AC861F202E3 completed with error: Error Domain=NSCocoaErrorDomain Code=260 "The file “version=1&uuid=CC95F08C-88C3-4012-9D6D-64A413D254B3&mode=compatible.jpeg” couldn’t be opened because there is no such file." UserInfo={NSURL=file:///Users/macbook/Library/Developer/CoreSimulator/Devices/276CD0A1-6FD0-4F01-ADD7-ED84023C8482/data/Containers/Shared/AppGroup/1B7CC31D-6A9C-4C80-B329-B7E6C132CC81/File%20Provider%20Storage/photospicker/version=1&uuid=CC95F08C-88C3-4012-9D6D-64A413D254B3&mode=compatible.jpeg, NSFilePath=/Users/macbook/Library/Developer/CoreSimulator/Devices/276CD0A1-6FD0-4F01-ADD7-ED84023C8482/data/Containers/Shared/AppGroup/1B7CC31D-6A9C-4C80-B329-B7E6C132CC81/File Provider Storage/photospicker/version=1&uuid=CC95F08C-88C3-4012-9D6D-64A413D254B3&mode=compatible.jpeg, NSUnderlyingError=0x600001830d20 {Error Domain=NSPOSIXErrorDomain Code=2 "No such file or directory"}}
Filename: IMG_0002
File Address: <PUPhotosFileProviderItemProvider: 0x6000038b5800> {types = (
"public.jpeg"
)}
Filename: IMG_0003
File Address: <PUPhotosFileProviderItemProvider: 0x6000038b5780> {types = (
"public.jpeg"
)}
Filename: IMG_0004
File Address: <PUPhotosFileProviderItemProvider: 0x6000038b5980> {types = (
"public.jpeg"
)}
Filename: IMG_0005
File Address: <PUPhotosFileProviderItemProvider: 0x6000038b5680> {types = (
"public.jpeg"
)}

原代码片段

import UIKit
import PhotosUI

class HomeVC: UIViewController {
    @IBOutlet weak var addFilesBarButtonItem: UIBarButtonItem!
    @IBOutlet weak var fileViewCollectionView: UICollectionView!
    
    private let cellIdentifier: String = "FilesViewCollectionViewCell"
    
    var importedMedia: [UIImage] = []
    var totalSelectedItems: Int = 0
    var progressLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        fileViewCollectionView.delegate = self
        fileViewCollectionView.dataSource = self
        
        self.fileViewCollectionView.register(UINib(nibName: "FilesViewCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: cellIdentifier)
        
        progressLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 50, height: 30))
        let leftBarButton = UIBarButtonItem(customView: progressLabel)
        navigationItem.leftBarButtonItems = [leftBarButton]
        progressLabel.isHidden = true // Hide initially
    }
    
    // MARK: - Function
    // Update progress label's text
    func updateProgressLabel(_ text: String) {
        DispatchQueue.main.async {
            self.progressLabel.isHidden = false
            self.progressLabel.text = text
            self.navigationController?.navigationBar.layoutIfNeeded()
        }
    }
    
    // Hide progress label
    func hideProgressLabel() {
        DispatchQueue.main.async {
            self.progressLabel.isHidden = true
            self.navigationItem.leftBarButtonItems = nil
            self.navigationController?.navigationBar.layoutIfNeeded()
        }
    }
    
    // MARK: - IBAction
    @IBAction func addFileBarButtonItemAction(_ sender: UIBarButtonItem) {
        var configuration = PHPickerConfiguration()
        configuration.selectionLimit = 0
        
        let picker = PHPickerViewController(configuration: configuration)
        picker.delegate = self
        present(picker, animated: true, completion: nil)
    }
}

// MARK: - UICollectionViewDataSource, UICollectionViewDelegate
extension HomeVC: UICollectionViewDelegate, UICollectionViewDataSource  {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return importedMedia.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as? FilesViewCollectionViewCell else {
            return UICollectionViewCell()
        }
        
        cell.filePhotoView.image = importedMedia[indexPath.row]
        return cell
    }
}


// MARK: - UICollectionViewDelegateFlowLayout
extension HomeVC: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
        
        cell.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
        
        UIView.animate(withDuration: 0.4, delay: 0.0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.8, options: .curveEaseOut, animations: {
            cell.transform = .identity
        }, completion: nil)
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        
        let width = (fileViewCollectionView.frame.size.width / 4) - 1
        let height = (fileViewCollectionView.frame.size.height / 8) - 1
        return CGSize(width: width, height: height)
    }
}

// MARK: - PHPickerViewControllerDelegate
extension HomeVC: PHPickerViewControllerDelegate {
    func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
        dismiss(animated: true)

        for result in results {
            result.itemProvider.loadObject(ofClass: UIImage.self) { object, error in
                if let image = object as? UIImage {
                    self.importedMedia.append(image)

                    if let url = result.itemProvider.suggestedName {
                        print("Filename: \(url)")
                        print("File Address: \(result.itemProvider)")
                    }
                }

                DispatchQueue.main.async {
                    self.fileViewCollectionView.reloadData()
                }
            }
        }
    }
}

问题原因分析

  1. 图片加载失败:日志中的Code=260错误表明某张图片的临时文件路径失效,导致loadObject(ofClass: UIImage.self)无法加载该图片,这张图片没有被添加到importedMedia数组,所以UICollectionView少显示一张。
  2. 多次无效刷新:原代码中每个图片加载完成(或失败)后都调用reloadData(),不仅会造成UI频繁刷新,也无法保证所有加载任务完成后统一刷新,容易出现数据不同步的情况。
  3. 缺少错误处理:加载失败时没有任何日志提示,无法定位问题所在。

修复方案与代码修改

核心修改点

  • 使用DispatchGroup跟踪所有图片加载任务,等待全部任务完成后再统一刷新UICollectionView
  • 优先使用loadFileRepresentation(forTypeIdentifier:)加载图片,该方法比loadObject更稳定,能有效避免临时文件路径失效的问题
  • 添加错误处理逻辑,打印加载失败的原因,便于调试
  • 利用进度标签实时展示加载进度,提升用户体验

修改后的PHPickerViewControllerDelegate代码

// MARK: - PHPickerViewControllerDelegate
extension HomeVC: PHPickerViewControllerDelegate {
    func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
        dismiss(animated: true)
        
        totalSelectedItems = results.count
        updateProgressLabel("0/\(totalSelectedItems)")
        
        let dispatchGroup = DispatchGroup()
        
        for (index, result) in results.enumerated() {
            dispatchGroup.enter()
            
            // 优先使用文件加载方式,稳定性更高
            if let uti = result.itemProvider.registeredTypeIdentifiers.first {
                result.itemProvider.loadFileRepresentation(forTypeIdentifier: uti) { url, error in
                    defer { dispatchGroup.leave() }
                    
                    // 处理加载错误
                    if let error = error {
                        print("加载失败 \(result.itemProvider.suggestedName ?? "未知文件"): \(error.localizedDescription)")
                        DispatchQueue.main.async {
                            self.updateProgressLabel("\(index+1)/\(self.totalSelectedItems)")
                        }
                        return
                    }
                    
                    // 验证文件路径并加载图片
                    guard let fileUrl = url, let image = UIImage(contentsOfFile: fileUrl.path) else {
                        print("无法获取图片 \(result.itemProvider.suggestedName ?? "未知文件")")
                        DispatchQueue.main.async {
                            self.updateProgressLabel("\(index+1)/\(self.totalSelectedItems)")
                        }
                        return
                    }
                    
                    // 主线程更新数据和进度
                    DispatchQueue.main.async {
                        self.importedMedia.append(image)
                        self.updateProgressLabel("\(index+1)/\(self.totalSelectedItems)")
                    }
                }
            } else {
                // fallback到loadObject方式
                result.itemProvider.loadObject(ofClass: UIImage.self) { object, error in
                    defer { dispatchGroup.leave() }
                    
                    if let error = error {
                        print("加载失败 \(result.itemProvider.suggestedName ?? "未知文件"): \(error.localizedDescription)")
                        DispatchQueue.main.async {
                            self.updateProgressLabel("\(index+1)/\(self.totalSelectedItems)")
                        }
                        return
                    }
                    
                    if let image = object as? UIImage {
                        DispatchQueue.main.async {
                            self.importedMedia.append(image)
                            self.updateProgressLabel("\(index+1)/\(self.totalSelectedItems)")
                        }
                    }
                }
            }
        }
        
        // 所有任务完成后刷新UI并隐藏进度标签
        dispatchGroup.notify(queue: .main) {
            self.hideProgressLabel()
            self.fileViewCollectionView.reloadData()
        }
    }
}

内容的提问来源于stack exchange,提问作者Shahriar Hossain

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 17:45:54