ARTICLE DETAIL

资讯详情

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

iOS相册多选与删除实战:权限、交互与PHPhotoLibrary避坑指南

iOS相册多选与删除实战:权限、交互与PHPhotoLibrary避坑指南 简介本资源面向iOS开发初学者与中级开发者聚焦相册图片多选与删除这一常见交互需求适用于社交、图片编辑类应用的开发场景。内容围绕第三方库QBImagePickerController展开讲解如何集成图片选择器、配置多选与最大选择数量、同步已选图片URL并结合自定义模型ShowEditItem管理选中数据。资源包共1个PDF文件大小约77KB以图文形式呈现完整实现思路涵盖拍照与相册选取的结合、删除照片后重新添加时的相册状态维护、数组操作逻辑以及图片压缩上传的尺寸调整与质量控制方法。目前已有288人学习下载适合希望快速掌握系统相册调用、多选删除交互与数据同步的开发者参考也可作为权限检查、错误处理与用户体验优化的实践起点。1. 相册多选与删除从系统能力边界到可落地实现做 iOS 相册多选和删除第一件要认清的事是你不可能绕过系统相册的授权模型去删照片。很多刚接触这个需求的开发者会想当然地以为只要拿到相册读取权限就能顺手把图删了结果真机一跑PHPhotoLibrary.shared().performChanges直接抛PHPhotosError.accessRestricted或者干脆静默失败。这不是玄学是 iOS 从 8 开始就把「读」和「写/删」拆成了两套权限语义到 iOS 14 又加了「有限访问」这一层权限状态从二值变成了三值甚至四值。这个标题要解决的核心问题其实有两块一是多选交互——怎么让用户像系统相册那样框选、连选、跨分组选二是删除落地——怎么把选中的PHAsset安全地提交给系统删除并且处理好失败回滚和 UI 同步。适合谁看正在做图片选择器、相册清理工具、社交 App 发帖选图模块的 iOS 开发者尤其是那些已经用上了QBImagePickerController这类第三方库、但发现删除环节还得自己接系统 API 的人。我先把结论摆前面多选用UICollectionView的allowsMultipleSelection配合自定义手势能覆盖 90% 场景删除必须走PHPhotoLibrary的performChanges且删除前一定要二次确认因为系统删除是进「最近删除」而非物理抹除用户后悔药只有 30 天。下面几章我会把权限、多选、删除、避坑、进阶验证一条线讲透。2. 权限与数据模型删除功能的前置条件2.1 读权限和写权限是两码事别混着申请iOS 相册权限在Info.plist里对应两个 keyNSPhotoLibraryUsageDescription读和NSPhotoLibraryAddUsageDescription写/增。注意删除属于「写」范畴但系统并没有单独的「删除权限」key删除能力依附在PHPhotoLibrary的变更权限上。也就是说用户如果只给了读权限你调删除一样会失败。从 iOS 14 开始读权限多了「有限访问」Limited状态。用户可能只授权你访问他挑出来的那几张图。这时候你的多选列表里能看到的PHAsset就是有限的删除也只能删这些。判断当前权限状态的标准写法import Photos func checkPhotoAuthorization() - PHAuthorizationStatus { // iOS 14 用 readWrite 参数低版本用 authorizationStatus() if #available(iOS 14, *) { return PHPhotoLibrary.authorizationStatus(for: .readWrite) } else { return PHPhotoLibrary.authorizationStatus() } } func requestPhotoAuthorization(completion: escaping (PHAuthorizationStatus) - Void) { if #available(iOS 14, *) { PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in DispatchQueue.main.async { completion(status) } } } else { PHPhotoLibrary.requestAuthorization { status in DispatchQueue.main.async { completion(status) } } } }逻辑说明.readWrite这个参数是关键它申请的是读写权限用户弹窗里会明确写「允许访问和修改照片」。如果你用.addOnly那只能往相册加图删不了。参数上requestAuthorization的回调不在主线程UI 更新必须切回 main。权限状态有五种.notDetermined、.restricted、.denied、.authorized、.limited。前三种基本没法删.limited要引导用户去「设置 → 你的 App → 照片 → 选中的照片」里扩权.authorized才是完全体。2.2 PHAsset 是句柄不是图片删除靠 localIdentifier很多人第一次用PHAsset会懵它不持有图片数据只是一个轻量句柄。你真正要删的时候靠的是它的localIdentifier。多选场景下我一般用一个SetString存选中的 identifier而不是存PHAsset对象本身因为PHAsset在相册变更后可能失效identifier 更稳。// 选中集合存 localIdentifier private var selectedIdentifiers SetString() // 从 PHFetchResult 里取资源 let fetchOptions PHFetchOptions() fetchOptions.sortDescriptors [NSSortDescriptor(key: creationDate, ascending: false)] let assets PHAsset.fetchAssets(with: .image, options: fetchOptions) // 遍历时把 identifier 存起来 assets.enumerateObjects { asset, _, _ in // asset.localIdentifier 就是删除时要用的钥匙 }参数说明PHFetchOptions的sortDescriptors按创建时间倒序和系统相册一致predicate可以过滤媒体类型比如只要图片就mediaType PHAssetMediaType.image。注意fetchAssets返回的是PHFetchResult它是懒加载的别一上来就enumerateObjects全量转数组几千张图会卡。提示localIdentifier在设备间不通用做云同步或跨端时不能拿它当业务主键只能当本地删除句柄。3. 多选交互用 UICollectionView 复刻系统相册的框选体验3.1 基础多选allowsMultipleSelection 只是起点UICollectionView自带allowsMultipleSelection true点一下选中、再点取消这是最省事的做法。但系统相册那种「长按进入选择模式、滑动连选、顶部显示已选数量」的体验光靠这个属性不够。我一般会加一个isEditing状态进入编辑模式后才开启多选避免和普通点击冲突。collectionView.allowsMultipleSelection false // 默认关闭 objc func enterEditMode() { isEditing true collectionView.allowsMultipleSelection true // 刷新导航栏显示取消和删除按钮 updateNavBarForEditing() } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { guard isEditing else { return } let asset assets[indexPath.item] selectedIdentifiers.insert(asset.localIdentifier) updateSelectedCountLabel() } func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { let asset assets[indexPath.item] selectedIdentifiers.remove(asset.localIdentifier) updateSelectedCountLabel() }逻辑说明didSelectItemAt和didDeselectItemAt是成对的选中集合的增删必须同步否则会出现「UI 上没勾但集合里有」的错位。updateSelectedCountLabel负责刷新「已选 N 张」的文案。3.2 滑动连选手势 indexPathsForVisibleItems 的配合系统相册最爽的是手指按住一划一排全选上。这个用UIPanGestureRecognizer实现。核心思路是手势移动时拿到当前触摸点对应的 indexPath判断是「选中」还是「取消」方向然后批量操作。objc func handlePan(_ gesture: UIPanGestureRecognizer) { let location gesture.location(in: collectionView) guard let indexPath collectionView.indexPathForItem(at: location) else { return } switch gesture.state { case .began: // 记录起始点的选中状态决定这一划是选还是取消 let asset assets[indexPath.item] isSelecting !selectedIdentifiers.contains(asset.localIdentifier) case .changed: // 只处理可见的 cell避免全量遍历 for visibleIndexPath in collectionView.indexPathsForVisibleItems { let asset assets[visibleIndexPath.item] if isSelecting { selectedIdentifiers.insert(asset.localIdentifier) collectionView.selectItem(at: visibleIndexPath, animated: false, scrollPosition: []) } else { selectedIdentifiers.remove(asset.localIdentifier) collectionView.deselectItem(at: visibleIndexPath, animated: false) } } default: break } }参数说明indexPathsForVisibleItems只返回当前屏幕内的 cell滑动时随着滚动会动态变化所以连选是「划到哪选到哪」。isSelecting这个布尔值在.began时定调保证一次滑动方向一致不会划着划着变成反选。注意手势要设cancelsTouchesInView false否则会吃掉 cell 的点击事件。3.3 第三方库 QBImagePickerController 的取舍热搜里出现了QBImagePickerController这是个老牌的多选库Objective-C 写的支持多选、限制最大数量、按相册分组。它的优点是接入快QBImagePickerController直接 push 出来就能用缺点是它只管选不管删删除还得你自己拿PHAsset去调系统 API。而且它对新系统「有限访问」的支持要看版本老版本在 iOS 14 上可能显示不全。我的建议如果只是要个选图界面QBImagePickerController 能省事但如果你要做「相册清理」这种删除是核心功能的 App自己用UICollectionViewPHFetchResult搭更可控因为你需要精确控制选中集合和删除后的 UI 刷新。选型上没有绝对对错看你的删除逻辑复杂度。4. 删除落地performChanges 的提交、回滚与 UI 同步4.1 删除 API 的正确调用姿势删除的唯一正道是PHPhotoLibrary.shared().performChanges里面调PHAssetChangeRequest.deleteAssets。注意它接收的是NSFastEnumeration可以传NSArray或PHFetchResult。func deleteSelectedAssets(completion: escaping (Bool, Error?) - Void) { // 用 identifier 重新 fetch 出 PHAsset避免持有失效对象 let fetchResult PHAsset.fetchAssets(withLocalIdentifiers: Array(selectedIdentifiers), options: nil) PHPhotoLibrary.shared().performChanges({ PHAssetChangeRequest.deleteAssets(fetchResult) }) { success, error in DispatchQueue.main.async { if success { self.selectedIdentifiers.removeAll() self.isEditing false self.reloadDataSource() // 重新拉取相册数据 } completion(success, error) } } }逻辑说明为什么不在闭包里直接用之前存的PHAsset数组因为从用户选中到点删除之间相册可能已经变了比如别的 App 删了图直接传旧对象可能失败。用fetchAssets(withLocalIdentifiers:)重新取一遍拿到的是当前有效的句柄。performChanges的 completion 在任意线程UI 操作必须切主线程。参数说明deleteAssets传PHFetchResult或[PHAsset]都行但传PHFetchResult性能更好它不会一次性把对象全实例化。删除成功后一定要reloadDataSource重新PHAsset.fetchAssets拉一遍否则 collectionView 的 indexPath 和底层数据会对不上直接崩。4.2 删除失败的三种典型错误与处理删除不是每次都成功常见错误码有三类错误触发场景处理方式PHPhotosError.accessRestricted权限不足或家长控制提示用户去设置开权限PHPhotosError.identifierNotFoundasset 已被其他进程删除忽略该条刷新列表PHPhotosError.userCancelled用户在系统弹窗点了取消不报错静默恢复 UIuserCancelled这个特别容易被忽略。iOS 在删除时如果涉及 iCloud 照片可能会弹一个系统确认框用户点取消success就是 falseerror是userCancelled。这时候你如果把选中状态清了用户会一脸懵——我明明没删成功怎么勾都没了。正确做法是只有success true才清空选中集合。PHPhotoLibrary.shared().performChanges({ PHAssetChangeRequest.deleteAssets(fetchResult) }) { success, error in DispatchQueue.main.async { if success { self.selectedIdentifiers.removeAll() self.reloadDataSource() } else if let error error as? PHPhotosError, error.code .userCancelled { // 用户取消保持选中状态不变 } else { self.showErrorAlert(error) } } }4.3 删除后的 UI 同步别让 indexPath 变成定时炸弹删除成功后底层PHFetchResult变了但 collectionView 还拿着旧的numberOfItems。如果你直接collectionView.deleteItems(at:)而不更新数据源下一次cellForItemAt就会越界崩溃。我的习惯是删除后整体 reload虽然动画糙一点但稳。func reloadDataSource() { let options PHFetchOptions() options.sortDescriptors [NSSortDescriptor(key: creationDate, ascending: false)] assets PHAsset.fetchAssets(with: .image, options: options) collectionView.reloadData() updateSelectedCountLabel() }如果非要保留删除动画那必须先用performBatchUpdates把数据源和 UI 一起改顺序不能错先改数据源数组再调deleteItems。但PHFetchResult不支持随机删除你得先转成[PHAsset]数组。转数组的代价是内存几千张图问题不大几万张就要慎重。注意删除操作如果涉及 iCloud 共享相册或「最近删除」里的内容行为会有差异。共享相册的删除是移除共享关系不是删原图别搞混。5. 避坑与排查那些让我加班到凌晨的相册删除问题5.1 现象删除后 App 闪退日志报 indexPath 越界原因删除成功后没有重新 fetch 数据源collectionView 的 item 数和PHFetchResult.count不一致cellForItemAt访问了不存在的 index。解决删除回调里先reloadDataSource()再reloadData()或者用performBatchUpdates严格同步数据源和 UI。我现在的习惯是删除后一律全量 reload不省这点动画。5.2 现象用户给了权限删除还是失败error 是 accessRestricted原因用户给的是「有限访问」Limited只能看到部分照片删除这些之外的照片会被拒。或者用户在系统设置里开了「屏幕使用时间」的内容限制。解决检测PHAuthorizationStatus.limited引导用户点「管理」去扩权。代码里用PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: viewController)直接拉起系统的有限库选择器。5.3 现象多选滑动时选中的 cell 高亮状态和集合对不上原因selectItem(at:)只改 UI 不改数据源didSelectItemAt只改数据源不改 UI两者没同步。滑动连选时如果只调了其中一个就会出现「勾了但没高亮」或「高亮了但没勾」。解决封装一个setSelected(_ selected: Bool, at indexPath: IndexPath)方法里面同时改selectedIdentifiers和调selectItem/deselectItem所有入口都走这个方法。5.4 现象删除大量照片时 App 卡死或内存暴涨原因一次性把几万张PHAsset转成数组或者performChanges里传了巨大的数组主线程被阻塞。解决分批删除每批 500 张左右用递归或DispatchGroup串行提交。PHFetchResult本身是懒加载的尽量别转数组。删除期间给个 loading 遮罩别让用户以为卡了。5.5 现象iOS 14 以下正常iOS 14 权限弹窗不出现原因Info.plist里只加了NSPhotoLibraryUsageDescription没加NSPhotoLibraryAddUsageDescription或者申请时用了旧的requestAuthorization()没带.readWrite。解决两个 key 都加上申请时用requestAuthorization(for: .readWrite)。iOS 14 如果只申请.addOnly弹窗文案是「添加照片」用户不会意识到你还要删。6. 进阶验证用 PHPhotoLibraryChangeObserver 做删除后的实时同步前面讲的删除都是「自己删自己刷新」但真实场景里用户可能切到系统相册删了图再切回你的 App这时候你的列表还是旧的。要解决这个得用PHPhotoLibraryChangeObserver。class AlbumViewController: UIViewController, PHPhotoLibraryChangeObserver { override func viewDidLoad() { super.viewDidLoad() PHPhotoLibrary.shared().register(self) } deinit { PHPhotoLibrary.shared().unregisterChangeObserver(self) } func photoLibraryDidChange(_ changeInstance: PHChange) { // 必须在主线程更新 UI DispatchQueue.main.async { guard let collectionChanges changeInstance.changeDetails(for: self.assets) else { return } // 用 changeDetails 后的 fetchResult 替换旧的 self.assets collectionChanges.fetchResultAfterChanges self.collectionView.reloadData() } } }逻辑说明photoLibraryDidChange会在相册任何变更时被调用包括你自己删的、别的 App 删的、iCloud 同步下来的。changeDetails(for:)返回一个PHFetchResultChangeDetails里面的fetchResultAfterChanges就是变更后的结果集。直接替换再 reload比手动算增删索引稳得多。参数说明register和unregister必须成对一般在viewDidLoad和deinit里。回调不在主线程UI 操作必须切 main。如果你的列表是分组的按日期或相册要对每个PHFetchResult分别调changeDetails。验证方法上我一般做三个测试一是自己删一张看列表是否实时少一张二是切到系统相册删一张再切回来看是否同步三是开 iCloud 照片在另一台设备删看本机是否收到变更。第三个最容易被忽略但恰恰是线上反馈最多的场景。说个我的血泪习惯任何涉及PHAsset的列表我都默认注册 changeObserver哪怕当前需求没要求实时同步。因为用户不会按你的预期操作他删完图切回来发现列表没变第一反应就是「这 App 有 bug」。提前接上 observer省掉后面一堆「为什么删了还在」的客诉。希望帮到你。本文还有配套的精品资源点击获取
返回列表