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.)