SwiftUI and UIImage memory leak

I’m experiencing significant performance and memory management issues in my SwiftUI application when displaying a large number of images using LazyVStack within a ScrollView. The application uses Swift Data to manage and display images.

Here’s the model I’m working with:

@Model
final class Item {
    var id: UUID = UUID()
    var timestamp: Date = Date.now
    var photo: Data = Data()
    
    init(photo: Data = Data(), timestamp: Date = Date.now) {
        self.photo = photo
        self.timestamp = timestamp
    }
}

extension Item: Identifiable {}
  • The photo property is used to store images. However, when querying Item objects using Swift Data in a SwiftUI ScrollView, the app crashes if there are more than 100 images in the database.
  • Scrolling down through the LazyVStack loads all images into memory leading to the app crashing when memory usage exceeds the device’s limits.

Here’s my view: A LazyVStack inside a ScrollView displays the images.

struct LazyScrollView: View {
    @Environment(\.modelContext) private var modelContext
    @State private var isShowingPhotosPicker: Bool = false
    @State private var selectedItems: [PhotosPickerItem] = []
    @Query private var items: [Item]

    var body: some View {
        NavigationStack {
            ScrollView {
                LazyVStack {
                    ForEach(items) { item in
                        NavigationLink {
                            Image(uiImage: UIImage(data: item.photo)!)
                                .resizable()
                                .scaledToFit()
                        } label: {
                            Image(uiImage: UIImage(data: item.photo)!)
                                .resizable()
                                .scaledToFit()
                        }
                    }
                }
            }
            .navigationTitle("LazyScrollView")
            .photosPicker(isPresented: $isShowingPhotosPicker, selection: $selectedItems, maxSelectionCount: 100, matching: .images)
            .onChange(of: selectedItems) {
                Task {
                    for item in selectedItems {
                        if let data = try await item.loadTransferable(type: Data.self) {
                            let newItem = Item(photo: data)
                            modelContext.insert(newItem)
                        }
                    }
                    try? modelContext.save()
                    selectedItems = []
                }
            }
        }
    }
}

Based on this:

  • How can I prevent SwiftUI from loading all the binary data (photo) into memory when the whole view is scrolled until the last item?
  • Why does SwiftUI not free memory from the images that are not being displayed?

Any insights or suggestions would be greatly appreciated. Thank you!

I will put the full view code in the comments so anyone can test if needed.

How can I prevent SwiftUI from loading all the binary data (photo) into memory when the whole view is scrolled until the last item?

This isn't a SwiftUI problem but an implementation detail from your code.

Storing the image binary data in core data doesn't seem like a great idea because on load, the binary data would all have to fit into memory and would be tied to your SwiftUI lifecycle.

The better approach here might be to store the images in the file system and retrieve them using their url.

Then you could use let uiImage = UIImage(contentsOf: myURL) which doesn't necessarily pull the image into memory and should be more performant.

Don’t store UIImages in your model, store the identifiers that you get from the photo picker (i.e. item.itemIdentifier). Then convert to images lazily.

Except…..

This doesn’t work if the user has granted “limited access” photo library permission. You’ll get identifiers, but you won’t be able to convert them to actual images. (That’s based on the UIKit version of the photo picker, but I guess it is the same here.)

So…. if you care about that, you do need to get the image data immediately. There is a note in the docs about it supplying PNG data. I don’t understand the details of that; if that applies here you’ll want to do whatever’s necessary to get JPEG or HEIC or something.

Then you need to ask yourself, if it’s unreasonable to have all the uncompressed images in RAM at the same time, is it reasonable or not to have all the compressed images in RAM? If that’s unreasonable, put them in the filesystem.

One other thing - you don’t seem to be using thumbnails for your labels, but rather using the full-size UIImage.

SwiftUI and UIImage memory leak
 
 
Q