ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

iOS相册多选与批量删除:Photos框架完整实现指南

iOS相册多选与批量删除:Photos框架完整实现指南 简介本资源面向iOS开发初学者与中级开发者聚焦相册图片多选与删除这一常见需求适用于社交、图片编辑类应用的开发场景。内容围绕第三方库QBImagePickerController展开讲解如何集成图片选择器、实现多选最多9张、拍照与相册选取结合、删除已选图片以及图片压缩上传等关键环节并涉及ShowEditItem数据模型对已选图片与URL集合的管理。资源包共1个文件为PDF格式大小约77KB内容以代码示例与实现思路为主便于对照学习。目前已有288人学习下载。通过阅读读者可掌握多选控制器的协议实现、代理回调处理、删除确认交互以及图片压缩优化等实用技巧为实际项目中的相册功能开发提供可复用的参考方案。1. iOS 相册多选与删除从 Photos 框架到可上架的完整实现很多 iOS 开发者第一次接到「相册多选 删除」需求时会下意识去翻 UIImagePickerController 的文档结果发现它压根不支持多选删除更无从谈起。这个功能真正落地的核心是Photos 框架Photos.framework配合PHPhotoLibrary的变更请求而不是 UIImagePickerController 或 PHPickerViewController 能单独搞定的。它要解决的问题很具体让用户在 App 内浏览系统相册、勾选多张图片、批量删除并且删除操作要同步回系统相册本身。适合已经能写基础 UIKit 界面、但对 Photos 框架的权限模型和变更机制不熟的中级开发者。下面按「权限 → 读取 → 多选 UI → 删除 → 避坑 → 进阶」的顺序把这条链路走通。2. 权限与相册读取先把 PHPhotoLibrary 的授权模型吃透2.1 三种授权级别决定了你能读到什么iOS 14 之后相册权限不再是简单的「允许/拒绝」而是分成了三档这个变化直接决定了多选界面的数据源范围授权级别枚举值可读取范围能否删除完全访问.authorized整个相册可以受限访问.limited仅用户手动选中的资源仅限已选中的拒绝.denied/.restricted无不可以.limited是 iOS 14 引入的「选择照片」模式用户只授权部分图片给 App。如果你的多选界面直接调PHAsset.fetchAssets拿全部资源在.limited下会拿到一个被系统裁剪过的集合很多开发者在这里翻车——以为代码写错了其实是权限范围本身就窄。请求授权的标准写法import Photos func requestPhotoAuthorization(completion: escaping (PHAuthorizationStatus) - Void) { // iOS 14 用 .readWrite老版本用 .authorized 判断 let status PHPhotoLibrary.authorizationStatus(for: .readWrite) switch status { case .notDetermined: PHPhotoLibrary.requestAuthorization(for: .readWrite) { newStatus in DispatchQueue.main.async { completion(newStatus) } } default: completion(status) } }逻辑说明authorizationStatus(for:)是同步查询当前状态.notDetermined才需要弹窗请求。回调不在主线程UI 更新必须切回 main。参数.readWrite表示你要读也要写删除属于写操作如果只读用.readOnly但删除功能必须用.readWrite否则performChanges会直接失败。提示Info.plist 里必须配置NSPhotoLibraryUsageDescriptioniOS 14 删除还需要NSPhotoLibraryAddUsageDescription吗不需要删除走的是 readWrite 权限但描述文案要写清楚「用于管理和删除照片」否则审核可能被拒。2.2 用 PHFetchOptions 精准拉取资源拿到权限后读取相册资源的核心是PHAsset.fetchAssets。多选界面通常需要按时间倒序、只取图片、限制数量func fetchRecentImages(limit: Int 200) - PHFetchResultPHAsset { let options PHFetchOptions() // 只取图片排除视频 options.predicate NSPredicate(format: mediaType %d, PHAssetMediaType.image.rawValue) // 按创建时间倒序最新的在前 options.sortDescriptors [NSSortDescriptor(key: creationDate, ascending: false)] // 限制数量避免一次性加载过多导致内存飙升 options.fetchLimit limit return PHAsset.fetchAssets(with: options) }逻辑说明predicate用mediaType过滤PHAssetMediaType.image的 rawValue 是 1。sortDescriptors按creationDate排序这是最常用的排序键。fetchLimit很关键——相册可能有几万张图不限制的话PHFetchResult虽然本身是懒加载但后续遍历生成缩略图时会拖垮性能。参数说明fetchLimit设多少取决于你的 UI一般一屏显示 3×412 张预加载 200 张足够滚动几屏。如果要做「加载更多」用PHFetchResult的objects(at:)分批取不要一次性全取。2.3 缩略图请求与缓存策略拿到PHAsset后不能直接显示必须通过PHImageManager请求缩略图let imageManager PHImageManager.default() func requestThumbnail(for asset: PHAsset, targetSize: CGSize, completion: escaping (UIImage?) - Void) { let options PHImageRequestOptions() options.deliveryMode .opportunistic // 先给低清再给高清 options.isNetworkAccessAllowed true // iCloud 图片允许下载 options.resizeMode .fast // 缩略图用 fast 模式 imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: options) { image, info in // 注意这个回调可能被调用多次 let isDegraded (info?[PHImageResultIsDegradedKey] as? Bool) ?? false if !isDegraded { completion(image) } } }逻辑说明deliveryMode .opportunistic会先返回一张低质量图degraded再返回高质量图回调触发两次。用PHImageResultIsDegradedKey判断只在非 degraded 时更新 UI避免闪烁。isNetworkAccessAllowed true让 iCloud 里的图也能下载否则用户看到的是空白。参数说明targetSize按 cell 实际像素算比如 cell 是 100×100 pt3x 就是 300×300。resizeMode .fast比.exact快很多缩略图场景够用。3. 多选交互实现UICollectionView 的状态管理3.1 选中状态的数据结构选型多选的核心是维护「哪些 asset 被选中」。常见做法有两种用SetString存localIdentifier或者用SetIndexPath存位置。我一般用前者因为删除时直接拿 identifier 去构造PHAsset不依赖 UI 位置final class PhotoPickerViewModel { private(set) var selectedIdentifiers SetString() private let maxSelectionCount: Int init(maxSelectionCount: Int 9) { self.maxSelectionCount maxSelectionCount } // 返回是否选中成功超过上限返回 false func toggleSelection(identifier: String) - Bool { if selectedIdentifiers.contains(identifier) { selectedIdentifiers.remove(identifier) return true } guard selectedIdentifiers.count maxSelectionCount else { return false // 达到上限UI 层弹提示 } selectedIdentifiers.insert(identifier) return true } func isSelected(_ identifier: String) - Bool { selectedIdentifiers.contains(identifier) } }逻辑说明SetString的查找和插入都是 O(1)比数组快。maxSelectionCount限制上限微信朋友圈是 9 张你可以按业务定。toggleSelection返回 Bool 让 UI 层知道是否成功失败时弹「最多选 9 张」的提示。参数说明localIdentifier是PHAsset的唯一标识格式类似B84E8479-475C-4727-A4A4-B77AA9980897/L0/001用它构造 asset 用PHAsset.fetchAssets(withLocalIdentifiers:options:)。3.2 Cell 的选中角标与序号显示多选界面通常要在 cell 右上角显示勾选状态选中后显示序号1、2、3…。序号需要根据选中顺序动态计算func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) - UICollectionViewCell { let cell collectionView.dequeueReusableCell(withReuseIdentifier: PhotoCell, for: indexPath) as! PhotoCell let asset fetchResult.object(at: indexPath.item) let identifier asset.localIdentifier requestThumbnail(for: asset, targetSize: cell.bounds.size) { image in cell.imageView.image image } let selected viewModel.isSelected(identifier) cell.setSelected(selected) if selected { // 序号 在选中集合中的位置 1 if let index viewModel.selectionOrder.firstIndex(of: identifier) { cell.orderLabel.text \(index 1) } } return cell }逻辑说明selectionOrder是一个[String]数组按选中顺序追加这样序号才是用户点击的顺序而不是 indexPath 顺序。setSelected控制角标显隐。参数说明序号 label 建议用圆形背景 白色数字直径 24pt 左右右上角偏移 (-4, 4)。3.3 全选与反选的边界处理全选看起来简单但有几个坑.limited权限下只能全选已授权的资源fetchResult可能很大全选要遍历所有 asset 的 identifierfunc selectAll() { selectedIdentifiers.removeAll() selectionOrder.removeAll() fetchResult.enumerateObjects { asset, _, _ in if selectedIdentifiers.count maxSelectionCount { selectedIdentifiers.insert(asset.localIdentifier) selectionOrder.append(asset.localIdentifier) } } }逻辑说明enumerateObjects遍历PHFetchResult比for i in 0..count更高效因为它是懒加载的。达到上限就停止插入避免超出。参数说明如果maxSelectionCount是 9全选后只选前 9 张UI 上要提示「已选 9 张达到上限」。4. 批量删除PHPhotoLibrary 变更请求的正确姿势4.1 删除必须走 performChangesPhotos 框架的所有写操作删除、收藏、修改都必须通过PHPhotoLibrary.shared().performChanges提交不能直接调 asset 的删除方法func deleteAssets(identifiers: [String], completion: escaping (Bool, Error?) - Void) { // 先根据 identifier 重新 fetch确保 asset 还有效 let fetchResult PHAsset.fetchAssets(withLocalIdentifiers: identifiers, options: nil) guard fetchResult.count 0 else { completion(false, NSError(domain: PhotoDelete, code: -1, userInfo: [NSLocalizedDescriptionKey: 没有可删除的资源])) return } PHPhotoLibrary.shared().performChanges({ // 删除请求传入要删除的 asset PHAssetChangeRequest.deleteAssets(fetchResult) }) { success, error in DispatchQueue.main.async { completion(success, error) } } }逻辑说明PHAssetChangeRequest.deleteAssets接收一个NSFastEnumerationPHFetchResult正好符合。删除是异步的回调在后台线程必须切回 main 更新 UI。success为 true 表示系统相册里已经删掉了。参数说明identifiers是localIdentifier数组。注意删除后这些 identifier 就失效了UI 上要从数据源移除。4.2 删除后的 UI 同步与数据源刷新删除成功后fetchResult不会自动更新必须重新 fetch 或者手动从数据源移除func handleDeleteSuccess(deletedIdentifiers: [String]) { // 从选中集合移除 deletedIdentifiers.forEach { viewModel.selectedIdentifiers.remove($0) viewModel.selectionOrder.removeAll { $0 $0 } } // 重新拉取相册数据 self.fetchResult fetchRecentImages() self.collectionView.reloadData() }逻辑说明重新 fetch 是最稳妥的做法因为删除后索引会变。如果数据量大可以用PHPhotoLibraryChangeObserver监听变更增量更新。参数说明PHPhotoLibraryChangeObserver的photoLibraryDidChange回调会带一个PHChange对象里面有changeDetails(for:)可以拿到增删改的索引适合做精细动画。4.3 删除失败的错误码排查删除失败常见错误码错误码含义排查方向-1自定义错误检查 identifiers 是否为空PHPhotosError.accessRestricted权限受限检查是否.limited且资源不在授权范围PHPhotosError.userCancelled用户取消系统弹窗被取消正常PHPhotosError.identifierNotFound资源不存在asset 已被其他 App 删除排查顺序先看权限状态再看 identifier 是否有效最后看是否在performChanges里抛了异常。5. 避坑与常见问题多选删除最容易翻车的 5 个点5.1 现象.limited权限下删除报错「资源不存在」原因用户只授权了部分图片你拿到的fetchResult是裁剪过的但删除时传入的 identifier 可能来自之前缓存的完整列表系统找不到。解决每次删除前用PHAsset.fetchAssets(withLocalIdentifiers:options:)重新校验count 0的直接跳过不要硬删。5.2 现象缩略图请求回调多次cell 图片闪烁原因deliveryMode .opportunistic会先给低清再给高清两次回调都更新了 UI。解决用PHImageResultIsDegradedKey判断只在非 degraded 时更新。或者改用.highQualityFormat只回调一次但加载慢。5.3 现象删除后 collectionView 崩溃「index out of range」原因删除后没有重新 fetchfetchResult.count还是旧值但 UI 已经 reloadcell 数量对不上。解决删除成功后必须重新 fetch 或手动同步数据源reloadData前确保fetchResult是最新的。5.4 现象iCloud 图片显示空白原因isNetworkAccessAllowed默认是 falseiCloud 里的图不会下载。解决设options.isNetworkAccessAllowed true并加一个 loading 占位图下载完成后再替换。5.5 现象审核被拒「缺少 NSPhotoLibraryUsageDescription」原因Info.plist 没配权限描述或者描述文案太模糊。解决加上NSPhotoLibraryUsageDescription文案写清楚用途比如「用于选择和管理您的照片支持批量删除」。6. 进阶用 PHPhotoLibraryChangeObserver 做增量刷新与性能优化前面删除后直接reloadData是最简单的做法但相册几千张图时体验很差——整个列表闪一下。更好的方案是注册PHPhotoLibraryChangeObserver监听系统相册变更做增量更新class PhotoLibraryObserver: NSObject, PHPhotoLibraryChangeObserver { var onChange: ((PHChange) - Void)? func photoLibraryDidChange(_ changeInstance: PHChange) { DispatchQueue.main.async { self.onChange?(changeInstance) } } } // 注册 let observer PhotoLibraryObserver() observer.onChange { [weak self] change in guard let self self, let fetchResult self.fetchResult else { return } if let details change.changeDetails(for: fetchResult) { // 更新 fetchResult self.fetchResult details.fetchResultAfterChanges // 批量更新 collectionView self.collectionView.performBatchUpdates { self.collectionView.deleteItems(at: details.removedIndexes?.map { IndexPath(item: $0, section: 0) } ?? []) self.collectionView.insertItems(at: details.insertedIndexes?.map { IndexPath(item: $0, section: 0) } ?? []) } } } PHPhotoLibrary.shared().register(observer)逻辑说明changeDetails(for:)返回一个PHFetchResultChangeDetails里面有removedIndexes、insertedIndexes、changedIndexes直接映射成IndexPath做批量更新。这样删除一张图只动一个 cell不会整个列表刷新。参数说明performBatchUpdates里 delete 和 insert 的顺序要注意先删后插否则索引会错位。如果details.hasIncrementalChanges为 false说明变化太大直接reloadData。还有一个性能细节缩略图请求要加缓存。PHImageManager本身有缓存但你可以用NSCacheNSString, UIImage再包一层key 用localIdentifier targetSize避免滚动时重复请求。我一般设countLimit 200totalCostLimit 50 * 1024 * 102450MB超过就自动淘汰。最后说个血泪教训删除操作一定要加二次确认弹窗。我见过太多 App 用户误触删除后找客服恢复而 Photos 框架删除是直接进「最近删除」相册30 天后才彻底清除。如果你要做「彻底删除」得用PHAssetChangeRequest.deleteAssets后提示用户去系统相册清空App 本身没有权限绕过「最近删除」。这个边界一定要在 UI 上写清楚否则用户以为删了就没了其实还能恢复反过来投诉你「删不干净」。希望帮到你。本文还有配套的精品资源点击获取
返回列表