Save a big batch of photos using the new Photos framework?
Asked Answered
C

4

17

I'm trying to save a big batch of photos into the Photos library using the new PHAssetChangeRequest class in iOS 8. Problem is, it looks like the daemon that saves the photos is itself crashing unexpectedly with a moderately large number of photos (I'm trying about 500). Anyone have any idea how to get around this limitation? Is it a memory usage problem in the daemon itself? It could also be a timeout limit on the change block, because in between the first 2 log statements below there's a not insignificant gap.

Shouldn't the assetsd daemon already be accounting for this use case since something like this is pretty much what the super complex model and design in the new Photos framework should have been able to handle? The documentation sample itself shows off the ability to save a photo.

Here's my code sample:

[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
    for (NSURL * url in fileURLs) {
        PHAssetChangeRequest * assetReq = [PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:url];
    }
    NSLog(@"Added %d assets",fileURLs.count);
} completionHandler:^(BOOL success, NSError *error) {
     if (!success){
         NSLog(@"%@",error);
     }
}];

And this is what my output looks like:

... Added 501 assets
... Connection to assetsd was interrupted or assetsd died
... Error Domain=NSCocoaErrorDomain Code=-1 "The operation couldn’t be completed. (Cocoa error -1.)

I even tried the synchronous performChangesAndWait method in PHPhotoLibrary but it also has the same problem.

I'm open to suggestions / ideas, am stuck! :(

Charisecharisma answered 3/10, 2014 at 16:29 Comment(9)
I am encountering a similar problem.I save 4 images, and it works, but if I try to save 4 images again to replace the existing ones I get this error. When I come up with a solution I'll get back to youMisestimate
@Misestimate I don't really see a way to "replace" existing photos in the Photos library. You could delete and add them again, I suppose.Charisecharisma
This error seems to be from the memory overloading. I reduced the file size of the UIImage and it fixed the problem. I can post code if you need to see it.Misestimate
Well no, not really interested in code for reducing file size. I need to save the original photos, not resized. I'll figure out a different way of doing it.Charisecharisma
I can guarantee its a memory exceeded error. Have you tried doing only a few at a time then clearing the cache? Like break it up into batches of 10 and clear your cache each time. Do you have a println in your didRecieveMemory function? If you run all 500, at what number does the memory function get triggered? Could you put a clear cache call in the didRecieveMemory warning?Misestimate
Hmm... I think, in my case the problem happens with adding a big batch of photos, together, whereas the daemon is not built to handle large count imports. Yes, I too struggled with determining memory requirements, but it was random. I had to implement a workaround where I save 1-by-1, track success and re-try failed ones on failure. The batch strategy did not work out. Apple might improve the daemon in the future (or at least I hope they do).Charisecharisma
@Misestimate Have you found the solution for this? I am facing the same issue, as you have mentioned in your comment, in iOS8 only. Please post solution if as soon as you find.Vetter
This was the best I came up with #26661034Misestimate
In 8.1 I receive the same error, even when saving a single image/video. However, the save seems to work.Hallo
R
3

The idea is doing it in small batches. PHPhotoLibrary::performChanges submits change requests once all together, so even it was able to finish, you won't be able to get any updates on the progress from a delegate or block. The 2017 WWDC session "What's New in Photos APIs" came with a sample app "Creating Large Photo Libraries for Testing" that does exactly that. Each performChanges has 10 images submitted, and UI updates are from the completion block of the performChanges, with a Semaphore blocking the thread until one batch is processed. I'm posting the important code pieces here:

private func addPhotos() {
    let batchSize = min(photosToAdd, maxBatchSize)
    if batchSize <= 0 || !active {
        isAddingPhotos = false
        active = false
        return
    }

    workQueue.async {
        let fileURLs = self.generateImagesAndWriteToDisk(batchSize: batchSize)
        self.createPhotoLibraryAssets(with: fileURLs)

        DispatchQueue.main.async {
            self.addPhotos()
        }
    }
}

private func createPhotoLibraryAssets(with imageFileURLs: [URL]) {
    photoLibrarySemaphore.wait() // Wait for any ongoing photo library

    PHPhotoLibrary.shared().performChanges({
        let options = PHAssetResourceCreationOptions()
        options.shouldMoveFile = true
        for url in imageFileURLs {
            let creationRequest = PHAssetCreationRequest.forAsset()
            creationRequest.addResource(with: .photo, fileURL: url, options: options)
        }
    }) { (success, error) in
        if !success {
            print("Error saving asset to library:\(String(describing: error?.localizedDescription))")
        }
        self.photoLibrarySemaphore.signal()
    }
}

Above generateImagesAndWriteToDisk is the method you need to replace with what ever your method to return a batch of 10 photo urls or so. I personally don't like writing in recursion. The code can be easily changed to non-recursion style.

Roderickroderigo answered 5/2, 2018 at 3:49 Comment(2)
I had the same problem and used a OperationQueue with maxConcurrentOperationCount set to 1Surfactant
Do you have any idea how to add Creation Date and Location data to the assets being created when using creationRequest.addResource(with: .photo, fileURL: url, options: options)?Parrisch
E
1

instead of creationRequestForAssetFromImageAtFileURL, I used this method and it worked perfect for 10 images (this part of code is repeated in a tableView:cellForRowAtIndexPath:)

    UIImage *thisImage=[UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[NSString stringWithFormat:@"%@%@",serverInfo,self.URLs[indexPath.row]]]]];
    [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
        PHAssetChangeRequest * assetReq = [PHAssetChangeRequest creationRequestForAssetFromImage:thisImage];
        NSLog(@"Added %ld assets",(long)indexPath.row);
    } completionHandler:^(BOOL success, NSError *error) {
        if (!success){
             NSLog(@"%@",error);
        }
}]; 
Ellene answered 15/4, 2015 at 9:7 Comment(0)
C
0

Processing a large batch of images on-device you have to be very careful about memory management even in the modern days of ARC. I had a large number of images to process (50+ with resizing) and ended up choosing the CGImageSourceCreateThumbnailAtIndex() as suggested by the answer here. It uses ImageIO which is supposed to be very efficient. The only issue I had remaining is that for some reason it would still hang on to memory unless I wrapped my for-loop in an @autoreleasepool {}

for (ALAsset *asset in assets) {
        @autoreleasepool {
            resizedImage = [self thumbnailForAsset:asset maxPixelSize:JPEG_MAX_DIMENSION];
        // do stuff here
    }
}
Cavetto answered 11/2, 2015 at 22:24 Comment(0)
T
-1

This isn't truly a solution for this question, however it is a workaround. You may still use the old ALAssetsLibrary to save files to the Camera Roll/Photos app successfully.

ALAssetsLibrary* lib = [[ALAssetsLibrary alloc] init];
[lib writeImageDataToSavedPhotosAlbum:imageData metadata:nil
                      completionBlock:^(NSURL *assetURL, NSError *error)
{
    // start saving your next image
}];

It is also recommended to use a single instance of the AssetsLibrary when looping through all the photos you want to save. You probably also want to wait for the first image save to complete to start saving the next.

You can then convert the resulting assetURL to a PHAsset if needed:

+ (PHFetchResult *)fetchAssetsWithALAssetURLs:(NSArray *)assetURLs
                                  options:(PHFetchOptions *)options
Topaz answered 9/1, 2015 at 4:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.