首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
iOS相册多选删除实战:Photos框架批量删除与UI同步
📅 2026/10/2 22:43:25
✍️ 爱科研究院
👁 阅读 3,247
简介这份PDF文档面向iOS开发初学者与中级开发者聚焦相册图片多选、删除及拍照上传这一常见业务场景帮助解决社交、图片编辑类应用中与系统相册交互、数据同步和图片压缩上传等实际问题。资源包共1个PDF文件大小约77KB内容以代码讲解与实现思路为主便于对照阅读和快速检索。文档围绕QBImagePickerController第三方库展开涵盖多选配置、最大选择数量控制、已选图片URL同步、代理回调处理、删除确认交互以及图片压缩上传等关键环节并给出RRZShowEditViewController等控制器的完整实现结构。目前已有288人学习适合需要快速搭建相册多选删除功能、理解第三方库集成与数据管理流程的开发者参考借鉴。1. 相册多选与删除从 Photos 框架到用户手指的那 0.5 秒做 iOS 相册功能最容易被低估的就是「多选 删除」这一对组合。单张选择谁都会写可一旦用户长按进入多选、手指在九宫格里来回勾选、最后点下那个红色删除按钮背后牵扯的是 PHPhotoLibrary 的变更请求、PHAsset 的批量引用、系统删除确认弹窗的二次授权以及删除后 UICollectionView 的索引错位。我见过太多项目在单张场景跑得好好的一上多选就出现「删了三张只掉了一张」「删完还剩占位图」「第二次进入选择态崩溃」这类玄学问题。这篇要解决的就是这条完整链路怎么用 Photos 框架拿到相册资源、怎么设计一套稳定的多选状态机、怎么把选中的 PHAsset 批量删除并让 UI 和数据源保持一致。适合已经能跑通单张选图、准备把相册模块做扎实的 iOS 开发者也适合正在被「删除后列表错乱」折磨的同学。下面按「先立住原理、再动手复现、最后讲坑」的顺序推代码可以直接抄进工程改。2. Photos 框架的权限与资源获取先把地基打对2.1 权限申请的三个层级与 Info.plist 配置Photos 框架的权限不是「给 / 不给」这么简单。iOS 14 之后新增了「受限访问」Limited用户可以选择只授权部分照片。如果你的代码只判断.authorized那在受限模式下会直接判定为无权限用户明明选了照片你的 App 却显示空白这是最常见的翻车点之一。需要在 Info.plist 里配置两个 key缺一个都会在调用时直接崩溃keyNSPhotoLibraryUsageDescription/key string需要访问相册以便您选择并管理图片/string keyNSPhotoLibraryAddUsageDescription/key string需要保存图片到您的相册/string读取权限用PHPhotoLibrary.authorizationStatus(for: .readWrite)注意参数是.readWrite而不是.read删除操作属于写权限范畴。判断逻辑要覆盖四种状态import Photos func ensurePhotoPermission(completion: escaping (Bool) - Void) { let status PHPhotoLibrary.authorizationStatus(for: .readWrite) switch status { case .authorized: completion(true) case .limited: // 受限访问仍可读取用户选中的资源删除需谨慎 completion(true) case .notDetermined: PHPhotoLibrary.requestAuthorization(for: .readWrite) { newStatus in DispatchQueue.main.async { completion(newStatus .authorized || newStatus .limited) } } case .denied, .restricted: completion(false) unknown default: completion(false) } }逻辑说明.limited必须当作可用状态处理否则受限用户完全无法使用相册功能。参数上.readWrite决定了后续能否执行删除如果只申请.readperformChanges里的删除请求会直接失败并抛出无权限错误。回调必须切回主线程因为后续要更新 UI。提示受限模式下如果用户想扩大授权范围可以调用PHPhotoLibrary.shared().presentLimitedLibraryPicker(from:)弹出系统选择器不要自己造轮子。2.2 用 PHAsset 拉取相册资源与排序拿到权限后用PHAsset.fetchAssets拉取资源。这里有个性能细节不要在主线程同步遍历几万张照片PHAsset是轻量引用但PHImageManager请求缩略图才是真正的开销。func fetchAllPhotos() - PHFetchResultPHAsset { let options PHFetchOptions() // 按创建时间倒序符合相册「最新在前」的直觉 options.sortDescriptors [NSSortDescriptor(key: creationDate, ascending: false)] // 只取图片过滤掉视频 options.predicate NSPredicate(format: mediaType %d, PHAssetMediaType.image.rawValue) return PHAsset.fetchAssets(with: options) }逻辑说明sortDescriptors用creationDate倒序是相册类 App 的通用做法用户预期最新照片在最上面。predicate过滤mediaType可以避免把视频混进来导致缩略图请求失败。PHFetchResult是懒加载的不会一次性把所有资源读进内存遍历时用object(at:)按需取。参数上如果要做分页加载可以配合PHFetchOptions.fetchLimit但相册场景一般不做分页因为用户会快速滑动分页反而增加复杂度。缩略图请求用PHImageManager.default().requestImage(for:targetSize:contentMode:options:)targetSize设成 cell 尺寸乘以屏幕 scaledeliveryMode设为.opportunistic兼顾速度和质量。3. 多选状态机的设计别用数组存选中项3.1 用 Set 管理选中索引而不是 Array多选最核心的数据结构选择直接决定了删除时会不会出错。很多人第一反应是用[IndexPath]或[Int]数组存选中项然后在删除时遍历数组去删数据源。这个做法在「删除后索引变化」的场景下必然翻车因为删掉前面的元素后后面所有索引都失效了。正确做法是用SetPHAsset或SetString存localIdentifier来管理选中项。Set的查找是 O(1)判断某个 cell 是否选中不需要遍历而且删除时按对象删而不是按索引删天然规避索引错位。final class PhotoPickerModel { private(set) var assets: [PHAsset] [] private(set) var selectedIds SetString() // 存 localIdentifier var isSelecting false // 是否处于多选态 func loadAssets() { let result fetchAllPhotos() var temp: [PHAsset] [] result.enumerateObjects { asset, _, _ in temp.append(asset) } assets temp } func toggleSelection(_ asset: PHAsset) { let id asset.localIdentifier if selectedIds.contains(id) { selectedIds.remove(id) } else { selectedIds.insert(id) } } func isSelected(_ asset: PHAsset) - Bool { selectedIds.contains(asset.localIdentifier) } }逻辑说明selectedIds存localIdentifier而不是PHAsset本身是因为PHAsset的isEqual依赖底层对象跨PHFetchResult重新拉取后引用可能不同而localIdentifier是稳定唯一的字符串。isSelecting控制是否显示勾选框单击进入预览还是切换选中状态由它决定。参数上如果要做「最多选 9 张」的限制在toggleSelection里加判断if !selectedIds.contains(id) selectedIds.count 9 { return }同时给用户一个轻提示。不要用assets.firstIndex(of:)去反查索引那是 O(n)列表长了会卡。3.2 UICollectionView 的选中态刷新与批量更新数据源用Set管好之后UI 刷新要避免全量reloadData。全量刷新会导致正在滑动的列表跳动而且丢失动画。正确做法是只刷新状态变化的 cell。func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let asset model.assets[indexPath.item] if !model.isSelecting { // 非多选态进入预览或大图 openPreview(asset) return } model.toggleSelection(asset) // 只刷新当前 cell带动画 collectionView.reloadItems(at: [indexPath]) updateBottomBar() } func updateBottomBar() { let count model.selectedIds.count deleteButton.isEnabled count 0 deleteButton.setTitle(删除(\(count)), for: .normal) }逻辑说明reloadItems(at:)只刷新指定 indexPath比reloadData轻量得多且保留滚动位置。updateBottomBar根据选中数量更新底部删除按钮的可用状态和文案这是多选交互的标准反馈。参数上如果 cell 数量很大比如上千张reloadItems依然只处理可见区域性能没问题。但要注意indexPath.item和model.assets的对应关系必须稳定所以assets数组在删除前不要做任何排序变化否则 indexPath 会错位。4. 批量删除的实现performChanges 与 UI 同步4.1 用 PHPhotoLibrary.performChanges 批量删除删除相册资源必须通过PHPhotoLibrary.shared().performChanges不能直接操作文件。系统会弹出确认弹窗用户确认后才真正删除。批量删除就是把所有选中的PHAsset一次性放进PHAssetChangeRequest.deleteAssets。func deleteSelectedAssets(completion: escaping (Bool) - Void) { let assetsToDelete model.assets.filter { model.isSelected($0) } guard !assetsToDelete.isEmpty else { completion(false) return } PHPhotoLibrary.shared().performChanges({ // 批量删除传 NSArray 形式的 PHAsset PHAssetChangeRequest.deleteAssets(assetsToDelete as NSArray) }) { success, error in DispatchQueue.main.async { if success { // 先从数据源移除再刷新 UI self.model.assets.removeAll { self.model.isSelected($0) } self.model.selectedIds.removeAll() self.collectionView.reloadData() self.updateBottomBar() } else { print(删除失败: \(String(describing: error))) } completion(success) } } }逻辑说明deleteAssets接收NSArraySwift 数组需要桥接。删除成功后必须先从model.assets移除对应元素再reloadData顺序反了会崩溃数据源和 UI 不一致。selectedIds要清空否则下次进入多选态会残留选中状态。参数上performChanges的 completion 回调不在主线程所有 UI 操作必须DispatchQueue.main.async包起来。错误处理里error可能是PHPhotosError常见的是用户取消或权限不足不要只打印最好给用户一个 toast 提示。4.2 删除后列表索引与占位图的处理删除后最容易出的问题是「列表还剩一个空白 cell」或「删完滚动位置跳到顶部」。前者是因为数据源移除了但 cell 没刷新后者是因为reloadData重置了 contentOffset。// 更平滑的做法用 performBatchUpdates 做局部删除 func deleteWithAnimation(at indexPaths: [IndexPath]) { collectionView.performBatchUpdates { // 先更新数据源 let idsToRemove indexPaths.map { model.assets[$0.item].localIdentifier } model.assets.removeAll { idsToRemove.contains($0.localIdentifier) } model.selectedIds.subtract(idsToRemove) // 再删 cell collectionView.deleteItems(at: indexPaths) } completion: { _ in self.updateBottomBar() } }逻辑说明performBatchUpdates里数据源变更和deleteItems必须成对出现且顺序是先改数据源再删 cell。subtract从选中集合里移除已删除的 id避免残留。这样滚动位置不会跳动画也自然。参数上indexPaths需要按降序排列再删否则删了前面的会导致后面的 indexPath 失效。如果嫌麻烦直接用reloadData也能跑但体验差一截。占位图问题通常是 cell 复用时没重置imageView.image nil在prepareForReuse里清空即可。5. 避坑与排查多选删除最容易翻车的 5 个点5.1 删除后崩溃数据源与 UI 不同步现象点删除后 App 直接 crash日志显示Invalid update: invalid number of items。原因是performBatchUpdates里数据源减少的数量和deleteItems的数量不一致或者先删了 cell 再改数据源。解决严格保证「先改数据源、再删 cell」且两者数量完全对应。用Set存选中项能大幅降低这类错误。5.2 受限权限下删除失败现象用户授权了「受限访问」选中照片后点删除没反应或报错。原因是受限模式下 App 只能读取用户指定的资源删除这些资源需要额外确认。解决在删除前判断authorizationStatus .limited如果是先提示用户「受限模式下删除可能需要在系统相册中确认」或者引导用户扩大授权。不要静默失败。5.3 选中状态在复用 cell 时错乱现象滑动列表后没选中的 cell 显示成选中状态或者选中的变回未选中。原因是 cell 复用时没有根据model.isSelected重置勾选框。解决在cellForItemAt里每次都根据model.isSelected(asset)设置勾选 UI不要依赖 cell 自身的状态。prepareForReuse里也要重置。5.4 删除大量照片时卡顿或超时现象一次删几百张照片界面卡住几秒甚至被系统杀掉。原因是performChanges里一次性提交太多资源系统处理慢。解决分批删除每批 50 到 100 张用递归或串行队列逐批提交。同时给用户一个 loading 指示避免以为卡死。5.5 删除后相册缓存未更新现象删除成功但重新进入相册页面还能看到已删除的照片。原因是PHFetchResult有缓存或者用了PHCachingImageManager没清缓存。解决删除后重新执行fetchAllPhotos()拉取最新数据或者监听PHPhotoLibraryChangeObserver的photoLibraryDidChange回调在回调里增量更新。不要依赖旧的PHFetchResult。6. 进阶用 PHPhotoLibraryChangeObserver 做增量刷新前面删除后直接reloadData是最省事的做法但列表长了会闪一下。更专业的做法是注册PHPhotoLibraryChangeObserver让系统在相册变化时主动通知你然后做增量更新。这个技巧在「用户在系统相册里删了照片你的 App 要同步」的场景下尤其有用。final class PhotoObserver: NSObject, PHPhotoLibraryChangeObserver { weak var model: PhotoPickerModel? weak var collectionView: UICollectionView? func startObserving() { PHPhotoLibrary.shared().register(self) } func photoLibraryDidChange(_ changeInstance: PHChange) { guard let model model, let collectionView collectionView else { return } // 用旧的 fetchResult 对比变化 let oldResult PHAsset.fetchAssets(with: model.fetchOptions) guard let changes changeInstance.changeDetails(for: oldResult) else { return } DispatchQueue.main.async { // 重新拉取最新数据 model.loadAssets() collectionView.reloadData() model.selectedIds.removeAll() } } deinit { PHPhotoLibrary.shared().unregisterChangeObserver(self) } }逻辑说明changeDetails(for:)会返回插入、删除、移动的详细信息理论上可以做精细的增量更新。但相册场景下变化往往涉及多个资源直接重新拉取再reloadData更稳妥代价是丢失滚动位置。如果要做极致体验可以结合changeDetails的removedIndexes和insertedIndexes做performBatchUpdates。参数上fetchOptions必须和拉取数据源时用的一致否则changeDetails对比会失败。deinit里一定要unregister否则观察者泄漏会导致崩溃。这个方案适合相册模块作为独立页面、生命周期明确的场景。我自己的习惯是删除操作永远走performChangesUI 刷新优先用performBatchUpdates做局部删除只有跨页面同步才上ChangeObserver。多选状态一律用SetString存localIdentifier这条血泪经验帮我省了至少三次线上崩溃。相册功能不难难的是边界情况把权限、索引、复用这三件事盯死基本就稳了。希望帮到你。本文还有配套的精品资源点击获取
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/10/2 22:43:25
cron表达式详解:从秒级到小时级,定时任务写法一次搞懂
2026/10/2 22:43:25
Fiddler+雷电模拟器安卓APP抓包全攻略:从HTTPS解密到实战调试
2026/10/2 22:38:25
Promise封装AJAX实战:从原理到落地,帮你彻底搞懂请求层设计
2026/10/2 23:33:28
2026年程序员AI工具链完整配置指南:从开发到部署全流程(TaoToken统一Key接入篇)
2026/10/2 23:33:28
AI Agent 接股票数据源实战:从 Tushare / AkShare 到 MCP 工具层,用 TaoToken 统一 Key 打通调用链
2026/10/2 23:33:28
高性价比AI论文工具势力榜(2026 终极指南):TaoToken 统一 Key 接入配置实战
2026/10/2 23:33:28
协议为什么存在-协议栈IP与MAC和缓冲区里的生产者消费者
2026/10/2 23:33:28
cywebdh导航主题6.1.0版本前瞻
2026/10/2 23:28:28
佳能复印机E000227-0001定影压力释放机构故障排查与修复指南
2026/10/2 0:01:33
Jev模型详解:从本地部署到Codex接入与数据系统构建
2026/10/2 0:01:33
Paperclip:轻量级AI Agent编排中间件实战指南
2026/10/2 0:01:33
DeepSpeed ZeRO-3 与 MoE 训练实战:显存优化与通信调优
2026/10/1 22:21:25
网站建设的英语怎么说?别只背单词,看完这套安全完整流程才敢上线
2026/10/2 12:21:42
新手入门看这篇:建设网站加盟避坑指南与SEO实操
2026/10/1 21:38:34
论文AIGC疑似度是什么意思?想查论文AI率有哪些免费工具?
2026/10/2 12:19:13
我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
2026/10/2 4:07:50
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026/10/2 6:07:10
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)