PHExternalAssetResource: Unable to issue sandbox extension for file.mov

I'm trying to add a video asset to my app's photo library, via drag/drop from the Photos app. I managed to get the video's URL from the drag, but when I try to create the PHAsset for it I get an error:

PHExternalAssetResource: Unable to issue sandbox extension for /private/var/mobile/Containers/Data/Application/28E04EDD-56C1-405E-8EE0-7842F9082875/tmp/.com.apple.Foundation.NSItemProvider.fXiVzf/IMG_6974.mov

Here's my code to add the asset:

let url = URL(string: videoPath)!
PHPhotoLibrary.shared().performChanges({
  PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: url)
    }) { saved, error in
        // Error !!
    }

Addictionally, this check is true in the debugger:

UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(videoPath) == true

Note that adding still images, much in the same way, works fine. And I naturally have photo library permissions enabled.

Any idea what I'm missing? I'm seeing the same error on iOS17.2 and iPadOS 17.2, with Xcode 15.2. Thanks for any tips ☺️

Answered by manuelC in 779372022

Ok got it working now ! 🎉

I was missing copying the contents to my own file, so we're still able to access it once the original is gone.

public func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: any UICollectionViewDropCoordinator) {
     
     for item in coordinator.items {
            // Check if the dropped item is a URL (for videos)
            if item.dragItem.itemProvider.hasItemConformingToTypeIdentifier(AVFileType.mov.rawValue) {
                item.dragItem.itemProvider.loadFileRepresentation(forTypeIdentifier: AVFileType.mov.rawValue) { (videoURL, error) in
                    if let droppedVideoURL = videoURL as? URL {
                        let fileManager = FileManager.default
                        let destination = fileManager.temporaryDirectory.appendingPathComponent("video123.mov")
                        try? fileManager.copyItem(at: droppedVideoURL, to: destination)

                        PHPhotoLibrary.shared().performChanges({
                            PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: destination)
                        }) { saved, error in
                            if saved {
                                print("SAVED!")
                            }
                        }
                    }
            }
      }
  }

Could you show more of your code, for example how exactly you're getting the videoPath? I believe that your process will only have access to that file within the completion block of the item provider. If you want to continue to have access to the data you will need to copy the contents to a file that you own. In your example you're passing the URL in a block to performChanges, in that case there's no guarantee that the file exists anymore.

Note the documentation here, in case this is what you're using: https://developer.apple.com/documentation/foundation/nsitemprovider/2888338-loadfilerepresentation

This method writes a copy of the file’s data to a temporary file, which the system deletes when the completion handler returns.

I use the UICollectionViewDropDelegate to get the dropped video URL:

public func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: any UICollectionViewDropCoordinator) {
     let videoSaver = VideoSaver()  
     
     for item in coordinator.items {
            // Check if the dropped item is a URL (for videos)
            if item.dragItem.itemProvider.hasItemConformingToTypeIdentifier(AVFileType.mov.rawValue) {
                item.dragItem.itemProvider.loadFileRepresentation(forTypeIdentifier: AVFileType.mov.rawValue) { (videoURL, error) in
                    if let droppedVideoURL = videoURL as? URL {
                        videoSaver.writeToPhotoAlbum(droppedVideoURL)
                    }
            }
      }
  }

Here's my VideoSaver:

final class VideoSaver: NSObject {

    func writeToPhotoAlbum(_ videoPath: URL) {
        // `isCompatible` is `true` when I test on my iPhone, but is `false` on the iPad Sim. 🙈
        let isCompatible = UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(videoPath.absoluteString) 

        PHPhotoLibrary.shared().performChanges({
            PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: videoPath)
        }) { saved, error in
             // This is the error:

             // (Error?) domain: "PHPhotosErrorDomain" - code: 18446744073709551615 {
             //  _userInfo = 0x0000000000000000
             // }
            if saved {
                print("SAVED!")
            }
        }
    }

On the iPad Simulator I additionally get this error from calling: UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(videoPath.absoluteString). I do NOT get this error on my real iPhone:

` ``Video file:///Users/manuel/Library/Developer/CoreSimulator/Devices/6A7D334E-A84E-4585-B7E0-CF2F02C76197/data/Containers/Data/Application/F23068E9-0517-4EF3-B82F-4E45D9C05745/tmp/.com.apple.Foundation.NSItemProvider.tZZ8Jd/IMG_0839.mov cannot be saved to the photo library: Error Domain=AVFoundationErrorDomain Code=-11800 "The operation could not be completed" UserInfo={NSUnderlyingError=0x600000c58210 {Error Domain=NSOSStatusErrorDomain Code=-17913 "(null)"}, NSLocalizedFailureReason=An unknown error occurred (-17913), NSLocalizedRecoverySuggestion=XXXXDEFAULTVALUEXXXX, NSURL=file:/Users/manuel/Library/Developer/CoreSimulator/Devices/6A7D334E-A84E-4585-B7E0-CF2F02C76197/data/Containers/Data/Application/F23068E9-0517-4EF3-B82F-4E45D9C05745/tmp/.com.apple.Foundation.NSItemProvider.tZZ8Jd/IMG_0839.mov -- file:///, AVErrorFailedDependenciesKey=( "assetProperty_CameraRollValidation" ), NSLocalizedDescription=The operation could not be completed}```

Accepted Answer

Ok got it working now ! 🎉

I was missing copying the contents to my own file, so we're still able to access it once the original is gone.

public func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: any UICollectionViewDropCoordinator) {
     
     for item in coordinator.items {
            // Check if the dropped item is a URL (for videos)
            if item.dragItem.itemProvider.hasItemConformingToTypeIdentifier(AVFileType.mov.rawValue) {
                item.dragItem.itemProvider.loadFileRepresentation(forTypeIdentifier: AVFileType.mov.rawValue) { (videoURL, error) in
                    if let droppedVideoURL = videoURL as? URL {
                        let fileManager = FileManager.default
                        let destination = fileManager.temporaryDirectory.appendingPathComponent("video123.mov")
                        try? fileManager.copyItem(at: droppedVideoURL, to: destination)

                        PHPhotoLibrary.shared().performChanges({
                            PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: destination)
                        }) { saved, error in
                            if saved {
                                print("SAVED!")
                            }
                        }
                    }
            }
      }
  }
PHExternalAssetResource: Unable to issue sandbox extension for file.mov
 
 
Q