iosswiftphotokit

How to batch edit multiple photos in iOS photo library using PHAssetChangeRequest


I'm trying to apply a transform edit to photos that a user selects from their photo library. I have the following code:

let options = PHContentEditingInputRequestOptions()
options.canHandleAdjustmentData = { adjustmentData -> Bool in
    adjustmentData.formatIdentifier == AdjustmentFormatIdentifier && adjustmentData.formatVersion == "1.0"
}
PHPhotoLibrary.shared().performChanges({
    for asset in imageAssets {
        asset!.requestContentEditingInput(with: options) {contentEditingInput, info in
            // Create a CIImage from the full image representation
            let url = contentEditingInput!.fullSizeImageURL!
            let orientation = contentEditingInput!.fullSizeImageOrientation
            var inputImage = CIImage(contentsOf: url, options: nil)!
            inputImage = inputImage.oriented(forExifOrientation: orientation)
            
            // Create the filter to apply
            let transformFilter = CIFilter.lanczosScaleTransform()
            transformFilter.inputImage = inputImage
            transformFilter.scale = 1
            transformFilter.aspectRatio = Float(stretchFactor)
            
            // Apply the filter
            let outputImage = transformFilter.outputImage
            
            // Create a PHAdjustmentData object that describes the filter that was applied
            let filterData = withUnsafeBytes(of: stretchFactor) { Data($0) }
            let adjustmentData = PHAdjustmentData(formatIdentifier: AdjustmentFormatIdentifier, formatVersion: "1.0", data: filterData)
            
            // Create a PHContentEditingOutput object and write a JPEG representation of the filtered object to the renderedContentURL
            let contentEditingOutput = PHContentEditingOutput(contentEditingInput: contentEditingInput!)
            let jpegData = UIImage(ciImage: outputImage!, scale: displayScale, orientation: .up).jpegData(compressionQuality: 1)
            do {
                try jpegData?.write(to: contentEditingOutput.renderedContentURL, options: .atomic)
            } catch { }
            contentEditingOutput.adjustmentData = adjustmentData
            let request = PHAssetChangeRequest(for: asset!)
            request.contentEditingOutput = contentEditingOutput
        }
    }
}, completionHandler: {success, error in
   if success {
       isPresented = false
   }
})

But when I try to save the edits, I get an error stating This method can only be called from inside of -[PHPhotoLibrary performChanges:completionHandler:]

If I move the PHAssetChangeRequest() line out of the requestContentEditingInput() function and directly into PHPhotoLibrary.shared().performChanges({}) then it works, but the issue is that iOS displays a separate permission alert to allow saving the edited photo for every single photo the user selected (i.e. they have to tap "Allow" 10 times in a row). Instead, I only want to show a single permission alert asking "Do you want to allow this app to modify 10 photos?". This is definitely possible as apps like Pixelmator Photo do exactly this. How is it done?


Solution

  • Your issue is due to the asynchronous nature of the PHAsset requestContentEditingInput method. This results in your calls to PHAssetChangeRequest being made long after the PHPhotoLibrary.shared().performChanges block is finished. In other words, you end not performing any changes within the scope of the performChanges block. This is why you get prompted over and over instead of once.

    The following rework of your code should work. This code iterates through the assets creating each of the corresponding PHContentEditingOutput instances. Once all of those asynchronous processes complete, only then is performChanges called and a set of PHAssetChangeRequest instances are created and setup.

    A DispatchGroup is used to wait until all asynchronous processes are complete.

    let options = PHContentEditingInputRequestOptions()
    options.canHandleAdjustmentData = { adjustmentData -> Bool in
        adjustmentData.formatIdentifier == AdjustmentFormatIdentifier && adjustmentData.formatVersion == "1.0"
    }
    
    var editingOutputs = [PHAsset:PHContentEditingOutput]()
    let group = DispatchGroup()
    for asset in imageAssets {
        group.enter()
        asset!.requestContentEditingInput(with: options) {contentEditingInput, info in
            defer { group.leave() }
    
            // Create a CIImage from the full image representation
            let url = contentEditingInput!.fullSizeImageURL!
            let orientation = contentEditingInput!.fullSizeImageOrientation
            var inputImage = CIImage(contentsOf: url, options: nil)!
            inputImage = inputImage.oriented(forExifOrientation: orientation)
    
            // Create the filter to apply
            let transformFilter = CIFilter.lanczosScaleTransform()
            transformFilter.inputImage = inputImage
            transformFilter.scale = 1
            transformFilter.aspectRatio = Float(stretchFactor)
    
            // Apply the filter
            let outputImage = transformFilter.outputImage
    
            // Create a PHAdjustmentData object that describes the filter that was applied
            let filterData = withUnsafeBytes(of: stretchFactor) { Data($0) }
            let adjustmentData = PHAdjustmentData(formatIdentifier: AdjustmentFormatIdentifier, formatVersion: "1.0", data: filterData)
    
            // Create a PHContentEditingOutput object and write a JPEG representation of the filtered object to the renderedContentURL
            let contentEditingOutput = PHContentEditingOutput(contentEditingInput: contentEditingInput!)
            let jpegData = UIImage(ciImage: outputImage!, scale: displayScale, orientation: .up).jpegData(compressionQuality: 1)
            do {
                try jpegData?.write(to: contentEditingOutput.renderedContentURL, options: .atomic)
            } catch { }
            contentEditingOutput.adjustmentData = adjustmentData
            editingOutputs[asset!] = contentEditingOutput
        }
    }
    
    group.notify(queue: .main) {
        PHPhotoLibrary.shared().performChanges({
            for (asset, contentEditingOutput) in editingOutputs {
                let request = PHAssetChangeRequest(for: asset)
                request.contentEditingOutput = contentEditingOutput
            }
        }, completionHandler: {success, error in
           if success {
           }
        })
    }