Starting with iOS 26.6, saving an image to the photo library via
PHAssetCreationRequest.addResource(with: .photo, data:, options:) appears to
re-encode the image, dropping all EXIF metadata. The same code preserved
metadata through iOS 26.5.
let creationRequest = PHAssetCreationRequest.forAsset()
creationRequest.addResource(with: .photo, data: image, options: nil)
I would like to know whether this change is intentional, and whether the file-based overload is the supported way to preserve an image's original metadata.
What we verified
-
The bytes we hand to PhotoKit still contain the metadata.
-
Setting
contentTypeexplicitly does not help. We resolved the type from the data and confirmed at runtime that it was non-nil (public.jpeg):
let options = PHAssetResourceCreationOptions()
if #available(iOS 26.0, *) {
options.contentType = contentType // verified: UTType.jpeg, not nil
}
let request = PHAssetCreationRequest.forAsset()
request.addResource(with: .photo, data: imageData, options: options)
- The resulting asset still has no EXIF.
- Writing the identical bytes to a temporary file and using the file-based overload preserves everything:
try imageData.write(to: temporaryFileURL, options: .atomic)
let options = PHAssetResourceCreationOptions()
options.shouldMoveFile = true
let request = PHAssetCreationRequest.forAsset()
request.addResource(with: .photo, fileURL: temporaryFileURL, options: options)
- Capture date, camera information and location are all intact.
We access the library with PHAccessLevel.addOnly.
I can reproduce this on iOS 26.7, and I do not see it on iOS 27.0.
I ran the same test app on three iPad builds, saving a JPEG carrying DateTimeOriginal, LensModel, UserComment, TIFF Make and Model, and a GPS block:
- iOS 26.5:
addResource(with:.photo, data:options:)preserved every tag. - iOS 26.7: the same call dropped DateTimeOriginal, LensModel, UserComment, Make and Model.
PHAsset.creationDatecame back as the import time rather than the capture date, andPHAsset.locationcame back nil. - iOS 27.0: every tag preserved, and
PHAsset.creationDateandPHAsset.locationboth matched the source.
The file-based overload returned the original bytes unchanged on all three versions, and shouldMoveFile made no difference to that. Setting contentType to UTType.jpeg made no difference on 26.7, which matches what you found.
One detail that may affect how you check this. On 26.7 the GPS block survived in the resource file with all four keys, while PHAsset.location was nil. The coordinates were still in the file even though the Photos database did not have them. PHAsset.location and the Photos info panel read the database rather than the file, so those two layers can disagree.
Since you are using PHAccessLevel.addOnly, your app cannot read the resource back to see which layer is affected. Requesting .readWrite in a debug build long enough to inspect the PHAssetResource data would separate the two.