iOS用Swift+PhotoKit替换图片遇PHPhotosErrorDomain 3303错误求助
在使用Swift+PhotoKit实现图片编辑后替换原图时,遇到PHPhotosErrorDomain error 3303,该错误对应PHPhotosErrorResourceLocked,通常是权限、资源不可编辑或操作流程错误导致。以下是具体问题分析和修复方案:
核心问题分析
1. 权限不足:有限授权无法编辑未选中照片
iOS 14+的照片权限分为Limited和Authorized,如果用户只授予有限权限,App仅能访问用户明确选中的照片。当前代码中fetchPhotos().first直接取最新照片,若该照片不在用户授权访问列表内,会触发3303错误。
2. 替换的Asset与编辑图片不匹配
当前替换逻辑直接取最新Asset,但用户选中编辑的可能并非这张照片,导致尝试编辑无关且无权限的Asset。
3. 在performChanges阻塞队列中调用异步请求
getEditingInput用信号量阻塞了performChanges的执行队列,而asset.requestContentEditingInput本身是异步操作,这种阻塞会引发PhotoKit资源锁冲突,触发3303错误。
4. 未设置输出文件格式标识
PHContentEditingOutput需明确设置uniformTypeIdentifier匹配原Asset格式,否则写入的文件不被PhotoKit认可。
5. 错误被吞掉:写文件失败未捕获
try? jpegData.write(to: ...)吞掉了写入错误,无法排查是否因文件写入失败导致后续错误。
修复后的完整代码
1. 修正权限检查与Asset匹配逻辑
在选择图片时同时保存对应的PHAsset,确保替换的是用户选中的照片:
修改ContentView的状态变量:
@State private var selectedImage: UIImage? = nil @State private var selectedAsset: PHAsset? = nil // 新增:保存选中的Asset
修改PhotoPicker的Coordinator,获取选中的Asset:
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { picker.dismiss(animated: true) guard let result = results.first else { return } // 获取对应的PHAsset if let assetIdentifier = result.assetIdentifier { selectedAsset = PHAsset.fetchAssets(withLocalIdentifiers: [assetIdentifier], options: nil).firstObject } if result.itemProvider.canLoadObject(ofClass: UIImage.self) { result.itemProvider.loadObject(ofClass: UIImage.self) { image, error in DispatchQueue.main.async { if let image = image as? UIImage { self.parent.selectedImage = image } } } } }
修改替换按钮的逻辑:
Button(action: { guard let asset = selectedAsset, let image = selectedImage else { return } replacePhoto(asset: asset, with: image) }) { Text("Replace Photo") }
2. 修正replacePhoto方法,避免阻塞队列
将获取editingInput的步骤放到performChanges外面,完善错误捕获:
func replacePhoto(asset: PHAsset, with image: UIImage) { // 检查权限是否为完全授权 let status = PHPhotoLibrary.authorizationStatus(for: .readWrite) guard status == .authorized else { print("需要完全相册权限才能替换原图,请前往设置修改权限") return } // 先获取editingInput,再执行变更 asset.requestContentEditingInput(with: nil) { editingInput, info in guard let editingInput = editingInput else { print("获取编辑输入失败: \(info?[PHContentEditingInputErrorKey] ?? "未知错误")") return } PHPhotoLibrary.shared().performChanges({ let editingOutput = PHContentEditingOutput(contentEditingInput: editingInput) // 设置与原Asset一致的格式 editingOutput.uniformTypeIdentifier = asset.value(forKey: "uniformTypeIdentifier") as? String ?? UTType.jpeg.identifier // 写入图片数据并捕获错误 do { guard let jpegData = image.jpegData(compressionQuality: 1.0) else { print("图片转JPEG数据失败") return } try jpegData.write(to: editingOutput.renderedContentURL, options: .atomic) } catch { print("写入图片数据失败: \(error.localizedDescription)") return } let request = PHAssetChangeRequest(for: asset) request.contentEditingOutput = editingOutput }) { success, error in DispatchQueue.main.async { if success { print("图片替换成功") } else { print("图片替换失败: \(error?.localizedDescription ?? "未知错误")") } } } } }
3. 更新Info.plist权限描述
添加读写权限描述(iOS 14+需要):
<key>NSPhotoLibraryUsageDescription</key> <string>需要访问您的照片以进行编辑和替换操作。</string> <key>NSPhotoLibraryAddUsageDescription</key> <string>需要保存编辑后的照片到您的相册。</string> <key>NSPhotoLibraryReadWriteAccessUsageDescription</key> <string>需要读写权限以替换原图。</string>
关键注意事项
- 权限要求:替换原图必须需要完全授权(.authorized),有限权限下仅能编辑用户明确选中的照片,且无法替换原图(只能保存为新照片)。
- Asset匹配:必须确保替换的是用户选中的Asset,而非随意获取的其他照片。
- 避免阻塞队列:不要在
performChanges的block内执行阻塞操作,所有异步准备工作需提前完成。 - 错误捕获:避免用
try?吞掉错误,明确捕获并打印错误信息,便于排查问题。
内容的提问来源于stack exchange,提问作者ThinkDeep

