解决PHPickerViewControllerDelegate的NSCocoaErrorDomain与图片显示缺失问题
问题描述
开发图片编辑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() } } } } }
问题原因分析
- 图片加载失败:日志中的
Code=260错误表明某张图片的临时文件路径失效,导致loadObject(ofClass: UIImage.self)无法加载该图片,这张图片没有被添加到importedMedia数组,所以UICollectionView少显示一张。 - 多次无效刷新:原代码中每个图片加载完成(或失败)后都调用
reloadData(),不仅会造成UI频繁刷新,也无法保证所有加载任务完成后统一刷新,容易出现数据不同步的情况。 - 缺少错误处理:加载失败时没有任何日志提示,无法定位问题所在。
修复方案与代码修改
核心修改点
- 使用
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

