This seems like super easy job, but I struggle to replace image after its edit.
First dummy UI to pick image from gallery:
import SwiftUI
import Photos
import PhotosUI
struct ContentView: View {
@State private var selectedImage: UIImage? = nil
@State private var isPickerPresented: Bool = false
var body: some View {
VStack {
if let image = selectedImage {
Image(uiImage: image)
.resizable()
.scaledToFit()
.frame(height: 300)
} else {
Text("No image selected")
}
Button(action: {
isPickerPresented = true
}) {
Text("Pick a Photo")
}
Button(action: {
selectedImage = editPhoto(selectedImage!)
}) {
Text("Apply Filter")
}
Button(action: {
savePhoto(selectedImage!)
}) {
Text("Save Photo")
}
Button(action: {
fetchPhotos().first.map { asset in
replacePhoto(asset: asset, with: selectedImage!)
}
}) {
Text("Replace Photo")
}
}
.onAppear {
requestPhotoLibraryAccess()
}
.sheet(isPresented: $isPickerPresented) {
PhotoPicker(selectedImage: $selectedImage)
}
}
}
struct PhotoPicker: UIViewControllerRepresentable {
@Binding var selectedImage: UIImage?
class Coordinator: NSObject, PHPickerViewControllerDelegate {
var parent: PhotoPicker
init(parent: PhotoPicker) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
guard let result = results.first else { return }
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
}
}
}
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
func makeUIViewController(context: Context) -> PHPickerViewController {
var configuration = PHPickerConfiguration()
configuration.selectionLimit = 1
configuration.filter = .images
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
// No updates required
}
}
getting permission:
func requestPhotoLibraryAccess() {
PHPhotoLibrary.requestAuthorization { status in
switch status {
case .authorized:
print("Access granted")
case .limited:
print("Limited access granted")
case .denied, .restricted:
print("Access denied")
case .notDetermined:
print("Not determined yet")
@unknown default:
fatalError("Unhandled authorization status")
}
}
}
while having this in info.plist
<key>NSPhotoLibraryUsageDescription</key>
<string>We need access to your photos to allow you to edit them.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>We need access to save the edited photos back to your library.</string>
Still not so important ways how to get image/asset (for replace), edit photo
func fetchPhotos() -> [PHAsset] {
var assets: [PHAsset] = []
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
let fetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
fetchResult.enumerateObjects { asset, _, _ in
assets.append(asset)
}
return assets
}
func editPhoto(_ image: UIImage) -> UIImage? {
return applyFilterWithFixedOrientation(
image: image,
filterName: "CIGaussianBlur",
parameters: [kCIInputRadiusKey: 20.0]
)
}
func applyFilterWithFixedOrientation(image: UIImage, filterName: String, parameters: [String: Any] = [:]) -> UIImage? {
guard let ciImage = CIImage(image: image) else { return nil }
// Create the filter
let filter = CIFilter(name: filterName)
filter?.setValue(ciImage, forKey: kCIInputImageKey)
// Set additional parameters if provided
for (key, value) in parameters {
filter?.setValue(value, forKey: key)
}
// Get the output image
if let outputImage = filter?.outputImage {
let context = CIContext()
if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
// Fix the orientation
return UIImage(cgImage: cgImage, scale: image.scale, orientation: image.imageOrientation)
}
}
return nil
}
func savePhoto(_ image: UIImage) {
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAsset(from: image)
}) { success, error in
DispatchQueue.main.async {
if success {
print("Photo saved successfully")
} else {
print("Failed to save photo: \(error?.localizedDescription ?? "unknown error")")
}
}
}
}
This is probably the not working part:
func replacePhoto(asset: PHAsset, with image: UIImage) {
PHPhotoLibrary.shared().performChanges({
guard let contentEditingInput = getEditingInput(for: asset) else {
print("Failed to retrieve editing input.")
return
}
// Prepare editing output
let editingOutput = PHContentEditingOutput(contentEditingInput: contentEditingInput)
// Save new image data to the editing output's URL
if let jpegData = image.jpegData(compressionQuality: 1.0) {
try? jpegData.write(to: editingOutput.renderedContentURL, options: .atomic)
}
// Update the asset with the new editing output
let request = PHAssetChangeRequest(for: asset)
request.contentEditingOutput = editingOutput
}) { success, error in
DispatchQueue.main.async {
if success {
print("Image replaced successfully.")
} else {
print("Failed to replace image: \(error?.localizedDescription ?? "Unknown error")")
}
}
}
}
func getEditingInput(for asset: PHAsset) -> PHContentEditingInput? {
let semaphore = DispatchSemaphore(value: 0)
var editingInput: PHContentEditingInput?
asset.requestContentEditingInput(with: nil) { input, _ in
editingInput = input
semaphore.signal()
}
semaphore.wait()
return editingInput
}
Because when I try to replace the photo I've picked, I got this error:
Failed to replace image: The operation couldn’t be completed. (PHPhotosErrorDomain error 3303.)
What am I missing or why I cannot save it to original place?
I finally figured out the issue:
3303
occurs when adjustmentData
of editingOutput
are not set (even with dummy data).3302
error appears.Final simplified (for posting) code that works for me looks like this:
if let firstAsset = fetchPhotos().first {
firstAsset.requestContentEditingInput(with: nil) { input, _ in
if let input {
if let url = input.fullSizeImageURL {
let orientation = input.fullSizeImageOrientation
if let inputImage = CIImage(contentsOf: url, options: nil)?.oriented(forExifOrientation: orientation) {
// with gray filter
if let editedPhoto = inputImage.applyFilterWithFixedOrientation(filterName: "CIPhotoEffectMono") {
let editingOutput = PHContentEditingOutput(contentEditingInput: input)
let adjustmentData = PHAdjustmentData(formatIdentifier: "", formatVersion: "1.0", data: "📸".data(using: .utf8)!)
editingOutput.adjustmentData = adjustmentData // this is not optional, but it is not required to be set, otherwise 3303 error will be thrown
do {
// try editedPhoto.heicData()?.write(to: editingOutput.renderedContentURL, options: .atomic)
if let jpegData = editedPhoto.jpegData(compressionQuality: 1.0) { // JPEG is mandatory otherwise 3302 error will be thrown
try jpegData.write(to: editingOutput.renderedContentURL, options: .atomic)
}
} catch {
print("Failed to write edited image: \(error.localizedDescription)")
}
if firstAsset.canPerform(.content) {
print("Can perform content editing")
}
PHPhotoLibrary.shared().performChanges({
let request = PHAssetChangeRequest(for: firstAsset)
request.contentEditingOutput = editingOutput
}, completionHandler: {success, error in
print("Success: \(success), Error: \(error?.localizedDescription ?? (success ? "" : "unknown error"))")
})
}
}
}
}
}
}