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?
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 {
}
})
}