WWDC22 SALE: Save 50% on all my Swift books and bundles! >>

SOLVED: How can I get SwiftUI to release cached image memory?

Forums > SwiftUI

I'm making a picture book app that, as you might expect, shows a lot of images that are stored locally. I'm using the standard SwiftUI Image type with a String provided by an Observed object.

For example:

@StateObject var storyManager = StoryManager()

var body: some View {
        VStack {
            Image(storyManager.currentImage)
              .resizable()
              .aspectRatio(contentMode: .fit)
          }

Each time the storyManager publishes a new string, the image changes. Each image adds to the app's memory footprint. SwiftUI will happily keep eating memory until the app crashes.

How can I get SwiftUI to release the image resources?

1      

I can confirm your experience: as long as new images are shown, the memory consumption seems to increase. I didn't check whether there is an upper limit and if yes, where it is.

But as soon as the app goes to the background the memory is released even though the app is not terminated.

Also if I trigger in the simulator "Debug > Simulate Memory Warning" the memory is released (atleast as soon as something happens in the app, e.g. when I go to the next picutre). This reduces the footprint back to arround 100 MB.


After testing on real devices: On my iPhone SE (2nd Gen with 3GB of RAM) the memory is automatically releases when the total used amount is getting over 30% of the total amount. On my iPhone 11 Pro the limit is closer to 20%....

After some more tests, I suppose the automatic release of the occupied memory happens if the amount of total free memory (for all processes running on the device) goes below 500 MB.


To my knowledge you have no control over the memory used for caching by the frameworks.

1      

After testing on real devices: On my iPhone SE (2nd Gen with 3GB of RAM) the memory is automatically releases when the total used amount is getting over 30% of the total amount. On my iPhone 11 Pro the limit is closer to 20%....

After some more tests, I suppose the automatic release of the occupied memory happens if the amount of total free memory (for all processes running on the device) goes below 500 MB.

Moving the app to the background does indeed clear up the memory cache, but sadly, if I keep using the app, the system keeps adding to the cache until the app crashes :(

I've tested on an iPhone 6s running 14.7.1 and an iPhone XS running 15.0, and while the RAM limits are different, they both keeping adding until a crash occurs.

To make it easier to test the effect vs running it in my actual app, I created this to demonstrate:

import SwiftUI

struct ContentView: View {
    var body: some View {
        NavigationView {
            VStack(spacing: 16) {
                NavigationLink(
                    destination: Text("Some other view"),
                    label: {
                        Text("Text view")
                    })

                NavigationLink {
                    NewTest()
                } label: {
                    Text("Image view")
                }
            }
        }
    }
}

struct NewTest: View {
    @State private var count = 0

    var body: some View {
        VStack {
            Image("image" + String(count))
                .resizable()
                .aspectRatio(contentMode: .fit)

            Text("Image\(count)")
                .padding()
                .background(Color.green)
                .foregroundColor(.white)
                .onTapGesture {
                    count += 1
                    showImageNum()
                }
        }
        .onAppear {
            showImageNum()
        }
    }

    func showImageNum() {
        print("image\(count)")
    }
}

I exported ~60 photos from Photos, converted them to .png, renamed them using Automator to be image0, image1, ...., and added them to the Asset catalog.

The only time I see the memory drop is when the app moves to the background.

@pd95 -- did your test app look significantly different than mine?

I'm wondering if having everything inside a NavigationView is making SwiftUI think it's all part of the same View and the memory should be held onto. I don't know how to refactor to avoid this issue.

1      

It seems that we had significant differences in our testing: I used JPEG photos (numbered 1 to 100), put them into the Asset catalog and had a simple Image view with a button (=no NavigationView involved).

I tested your code on my iPhone 13 Pro and I confirm the memory use is increasing more quickly and crashes at some point. Using JPEG images I didn't have a single crash in 100 images! With PNGs the app crashes after 25 images (at latest).

I have added monitoring of the "Memory Warning Notification" as below:

.onReceive(NotificationCenter.default.publisher(for: UIApplication.didReceiveMemoryWarningNotification, object: nil)) { notification in
    print(notification)
}

Testing your app again, you will see that you have received multiple memory warnings before your app crashes.

Using JPEG compressed images, I get my first memory warning after 91 images, the more after 103, 107, 112, 113 and the app crashes after 118 images.

Testing with my code again: Whenever a memory warning is received, the memory occuppied by the JPEG images is released immediately... now testing again with PNGs :-)

1      

OK, there really is some issue with SwiftUI Images: It doesn't release the cached resources when memory pressure increases. UIKit is not doing this properly.

To solve your issue, you can replace Image(String(count)) with Image(uiImage: UIImage(named: String(count))!): Loading the image using UIImage(named:) and then displaying it using Image.

The memory consumption of your app will still be high because the images are cached in CoreGraphics, but after a memory warning is given by the system CoreGraphics is freeing its caches!

So probably this would be again something to file a feedback for Apple: https://feedbackassistant.apple.com

BTW: I am now watching WWDC18 session iOS Memory Deep Dive which introduces the tooling to examine what we see in your application

1      

@pd95 -- It was a holiday here in Japan yesterday, so my apologies for being slow to reply.

I cannot tell you how relieved I am you found this solution!! I was starting to dig how to rebuild my view in UIKit (it's crazy how much of it I've forgotten since focusing on SwiftUI!), but being able to keep a SwiftUI solution is amazing.

More than that, thank you for taking the time to validate my problem and confirm it wasn't just me writing bad code. And thank you sharing your approach and testing the code that I shared as well to really home in on the problem. Beyond solving the immediate problem, it helped with my overall confidence in this project, which I appreciate more than I can properly express :)

1      

I've just read in UIImage(named:) initializer Special Considerations: If you intend to display an image only once and don't want it added to the system’s cache, create it using the imageWithContentsOfFile: method instead. Keeping single-use images out of the system image cache can potentially improve the memory use characteristics of your app.

This way you won't have caching at all. But this would also mean that you cannot use the asset catalog to store your images. (at least I have not found a way to get a "file path" pointing into the catalog content)

https://developer.apple.com/documentation/uikit/uiimage/1624146-init#1674004

1      

Hacking with Swift is sponsored by Fernando Olivares

SPONSORED Fernando's book will guide you in fixing bugs in three real, open-source, downloadable apps from the App Store. Learn applied programming fundamentals by refactoring real code from published apps. Hacking with Swift readers get a $10 discount!

Read the book

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.