4

I'm trying to load an image store in locally and then delete original file.

so here the code:

UIImage *workingCopy = [UIImage imageWithContentsOfFile:workingCopyPath];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *error = nil;
[fileManager removeItemAtPath:workingCopyPath error:&error];

after that I load this image into my view (OpenGL). Image looks like black square. Without deleting original file everything works perfect.

Is there any way to load image fully to memory and delete original file without damaging loaded image?

I've tried to use imageNamed but it doesn't work for me (just can't find file, probably because of extension)...

Image path is like: /Documents/SavedImages/IMG-186998396.png.working_copy

I pass NSString IMG-186998396.png.working_copy to imageNamed but it can't load the image.

Duncan Babbage
  • 19,972
  • 4
  • 56
  • 93
OgreSwamp
  • 4,602
  • 4
  • 33
  • 54

2 Answers2

4

UIImage uses caching mechanism and lazy loading (the image is note loaded unless it's rendered). Maybe postponing the deletion after it's displayed, or force it to render by drawing it to a CGGraphicContext

Better yet : you can use a background thread for doing the force loading, see the answer on that question

Community
  • 1
  • 1
Olotiar
  • 3,225
  • 1
  • 18
  • 37
0

The answer to this issue surfaces some interesting things about the way both UIImage and cgImage work. A UIImage object does not directly hold the image data, but rather holds a reference to external storage of the data.

As Apple's Documentation notes you can access this via either of the cgImage or ciImage properties on UIImage, so you would be forgiven for thinking that accessing these properties and calling myImage.cgImage.copy() would give you a copy of this data. It doesn't. Copying the cgImage and ciImage structs gives you a copy of those structs, but the copies still point to the original data store—in this case, the file on disk.

What this means is you get what initially seems fairly surprising results. You can create the UIImage successfully from disk, delete the file on disk, and you can still return a non-optional UIImage which "works" quite happily, other than the minor practicality that the image no longer can be used to access or display useful data.

The answer is to "deep copy" the underlying data in the process of creating a new UIImage. One way to make this easy to use is to create an extension on UIImage itself:

extension UIImage {
    func deepCopy() -> UIImage? {
        UIGraphicsBeginImageContext(self.size)
        self.draw(in: CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height))
        let copiedImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return copiedImage
    }
}

This works great. Apart from the graphics context consuming significant amounts of memory, if you use this in a loop. Which is interesting because while you were in the past required to do some manual memory management in a situation like this, as of Xcode 12.5 if you try you are told that foundation manages the memory of these objects. It appears true, insofar as these objects are not technically leaked. But they are also not released on a useful timescale and the memory is counted against your own app. Presumably these allocations would be drained by the system eventually, but if so not fast enough to loop over several thousand images, and copy them from disk into Core Data management in my experience... without your app being shutdown due to memory use. Never fear, there is a somewhat better alternative:

extension UIImage {
    func deepCopy() -> UIImage? {
        guard let data = self.pngData() else { return nil }
        return UIImage(data: data, scale: self.scale)
    }
}

Which does not use memory as fast as the first approach... though it still builds up enough to need to consider how to actively manage it for large collections. But for single images or small numbers, it can be used thusly:

if let FileManager.default.fileExists(atPath: workingCopyPath),
   let workingCopy = UIImage(contentsOfFile: imagePath),
   let securedImage = workingCopy.deepCopy() {
    do {
        try FileManager.default.removeItem(atPath: imagePath)
        return securedImage
    } catch let error {
        print(error.localizedDescription)
    }
}

There is of course a reason why UIImage doesn't work this way by default, or even ship a method for doing this. You are reading the data into memory (and in the first case actually drawing the image) in order to create the deep copy. This is necessary for what is intended in this question—it's just important to be mindful of when to use this approach given the performance implications it will have.

If using this approach in a loop, you need to take a couple of additional steps to actively manage memory. The most important thing is to wrap the process in a local autoreleasepool, which is trivial. However, the memory from this pool will only be drained when your loop is finished, so if you are processing a lot of images, then batch them. Here's one example of how you can do this, using an enumerator but only processing 50 images in the inner loop each time:

while try FileManager.default.contentsOfDirectory(atPath: path).count > 0 {
    let enumerator = FileManager.default.enumerator(atPath: peopleImagesURL.path)

    autoreleasepool {
        var count = 0
        while let fileName = enumerator?.nextObject() as? String {
        // Batch to only 50 images before draining pool, to cap memory use.
        guard count < 50 else { break }
        count += 1
        if let workingCopy = UIImage(contentsOfFile: imagePath),
           let securedImage = workingCopy.deepCopy() {
        do {
                ... // do whatever you are going to do with the secured image in the loop.
                try FileManager.default.removeItem(atPath: imagePath)
            } catch let error {
                print(error.localizedDescription)
            }
        }
    }
}

One limitation of this code example is it does not handle a scenario where your inner loop fails to remove an image for some reason. For instance, if you are unable to write a particular image to a database for some reason, throwing an error in ... above, for that file you would never reach FileManager.default.removeItem(atPath:). As a result, the outer loop would always find more than zero files in the directory, and after all files are process you will continually re-loop on the inner loop, potentially re-throwing the same error repeatedly if that particular file can never be processed. If this is a possibility, it would make sense to redesign the above to account for this.

(Kudos to the original Stack Overflow answer here and Swift version here that was the basis for this code. They answered a related question where the poster had a different reason for wanting to do a deep copy, but their own use case was in fact the same as the question here. Blame for suggesting implementing this as an extension on UIImage lies with me. On that note, you can actually drop all the self references in the extension and it'll work just fine... just seemed slightly clearer in a code example to include them.)

Duncan Babbage
  • 19,972
  • 4
  • 56
  • 93