How to limit memory usage of a list containing a large number of images / rows

I'm playing with SwiftUI and I'm currently struggling with images, especially in lists. Basically, I want to display an list of images with an infinite scroll but I want to keep the memory usage reasonable.

I have the following (truncated) code:

Code Block swift
struct HomeView: View {
@State private var wallpapers: Loadable<[Wallpaper]> = .notRequested
@State private var currentPage = 1
@Environment(\.container) private var container
var body: some View {
content
.onAppear { loadWallpapers() }
}
private var content: some View {
VStack {
wallpapersList(data: wallpapers.value ?? [])
/* ... snip ... */
}
}
private func wallpapersList(data: [Wallpaper]) -> some View {
ScrollView {
LazyVStack(spacing: 5) {
ForEach(data) { w in
networkImage(url: w.thumbs.original)
.onAppear { loadNextPage(current: w.id) }
}
}
}
}
private func networkImage(url: String) -> some View {
/* I use https://github.com/onevcat/Kingfisher to handle image loading */
KFImage(URL(string: url))
/* ... snip ...*/
}
private func loadWallpapers() {
container.interactors.wallpapers.load(data: $wallpapers, page: currentPage)
}
private func loadNextPage(current: String) {
/* ... snip ... */
}
}


Code Block swift
struct WallpapersInteractor: PWallpapersInteractor {
let state: Store<AppState>
let agent: PWallpapersAgent
func load(data: LoadableSubject<[Wallpaper]>, page: Int) {
let store = CancelBag()
data.wrappedValue.setLoading(store: store)
Just.withErrorType((), Error.self)
.flatMap { _ in
agent.loadWallpapers(page: page) /* network call */
}
.map { response in
response.data
}
.sink { subCompletion in
if case let .failure(error) = subCompletion {
data.wrappedValue.setFailed(error: error)
}
} receiveValue: {
if var currentWallpapers = data.wrappedValue.value {
currentWallpapers.append(contentsOf: $0) /* /!\ */
data.wrappedValue.setLoaded(value: currentWallpapers)
} else {
data.wrappedValue.setLoaded(value: $0)
}
}
.store(in: store)
}
}


Because I append the new data to my Binding every time I request a new batch of images, the memory consumption quickly becomes stupidly high.

I tried to remove data from the array using .removeFirst(pageSize) once I get to the third page so that my array contains at most 2 * pageSize elements (pageSize being 64 in this case). But doing so makes my list all jumpy because the content goes up, which creates more problems than it solves.

I tried searching for a solution but I surprisingly didn't find anything on this particular topic, am I missing something obvious ?
Answered by OOPer in 651687022
Thanks to the runnable project and good explanations, I could have checked the behavior with relatively short time.
  • Memory consumption grows with the list size grows, even if each row has no image. But the rate is very low.

  • When images included, memory consumption grows very rapidly.

Below includes some guesses based on the observed fact:
  • LazyVStack adds rows on demand, that may update the view structure (so called view graph) which is causing the high memory consumption.

  • Each KFImage holds an instance of ImageBinding, which has a property image of type KFCrossPlatformImage?. That is not freed even if the row is disappeared. (KFCrossPlatformImage is UIImage on iOS.)

Thus, to reduce memory consumption, you may need to define your own Image view, of which you can purge the loaded image when disappeared.
If that can help understand how all this work:
https://stackoverflow.com/questions/56655421/does-the-list-in-swiftui-reuse-cells-similar-to-uitableview

But you probably know already.

I did not look at your code in detail, but it seems you try to play against the system. May be that is just a wrong impression.

pageSize being 64 in this case

128 images in an Array? Too large to hold in an Array, even in UIKit apps.
The core parts of your code are hidden, but you should better not store large number of images into an Array.

128 images in an Array? Too large to hold in an Array, even in UIKit apps.

I do not store the images in the array, only the url fetched from the remote api. Images are loaded in the LazyVStack but are not stored in my array.

Thanks for the link Claude, I did not know it worked this way, I'm very new to SwiftUI. I think I understand what you mean by "try to play against the system" but I don't understand why a Lazy*Stack/Grid does not releases loaded-but-unseeable rows :/

I do not store the images in the array, only the url fetched from the remote api. Images are loaded in the LazyVStack but are not stored in my array. 

Thanks for explanation. If you could show code how you were managing images, that might be a better info.
This is pretty much everything, images are displayed here:

Code Block Swift
    private func wallpapersList(data: [Wallpaper]) -> some View {
        ScrollView {
            LazyVStack(spacing: 5) {
                ForEach(data) { w in
                    networkImage(url: w.thumbs.original)
                        .scaledToFit()
                        .frame(maxHeight: 300)
                        .onAppear {
                            loadNextPage(current: w.id)
                        }
                }
            }
        }
    }
    private func networkImage(url: String) -> some View {
        KFImage(URL(string: url))
            .resizable()
            .placeholder { ProgressView().progressViewStyle(CircularProgressViewStyle()) }
            .cancelOnDisappear(true)
    }


I call wallpapersList(data:) with a list of Wallpaper which contains the url I provide KFImage(_), fetching the image and displaying it in my LazyVStack. There's not much to it, it is very very basic.

wallpapersList(data:) is called in my var body: some View either with the result of the API call containing the [Wallpaper] either with an empty Array

it, it is very very basic

Basic for you, mystery for readers.
You're right, my bad, what I mean by basic is that it all boils down to a LazyVStack displaying an infinite list of Image(), there are no fancy tricks to make it work and no fancy UI, it really is very basic in terms of conception, at least in the code I provided, which is where my problems arise :)

You can find all the code on GitHub if that helps.

 If you could show code how you were managing images, that might be a better info

I'm not sure what I can show you more but I'll try:

Code Block Swift
/* This is the data structure I fetch from the remote API, nothing fancy here; no image either */
struct Wallpaper: Identifiable, Codable {
    let id: String
    let width: Int
    let height: Int
    let path: String
    let thumbs: Thumbs
}
extension Wallpaper {
    enum CodingKeys: String, CodingKey {
        case id = "id"
        case width = "dimension_x"
        case height = "dimension_y"
        case path = "path"
        case thumbs = "thumbs"
    }
}
struct Thumbs: Codable {
    let original: String
}



Code Block Swift
struct HomeView: View {
/* This binding contains the data fetched from the API. */
@State private var wallpapers: [Wallpaper] = []
/* Keep track of the pages we fetch, API pagination starts at one, so do we */
@State private var currentPage = 1
    var content: some View {
/* Images are displayed here, the wallpapersList(data:) function iterates over the binding's data; or an empty array if no data was found */
        wallpapersList(data: wallpapers.value ?? [])
/* When the View appears, we load the first batch of images from the API */
            .onAppear { loadWallpapers() }
    }
}
/* This function will return a scrollview containing the images */
    private func wallpapersList(data: [Wallpaper]) -> some View {
        ScrollView {
/* This is the images container */
            LazyVStack(spacing: 5) {
/* For each wallpaper we fetched... */
                ForEach(data) { w in
/* We call the networkImage(url:) function, which returns a KFImage, displaying an image fetched over network */
                    networkImage(url: w.thumbs.original)
                        .scaledToFit()
                        .frame(maxHeight: 200)
                }
            }
        }
    }
/* This function is just a wrapper around KFImage */
/* KFImage will return an Image loaded over network */
    private func networkImage(url: String) -> some View {
        KFImage(URL(string: url))
            .resizable()
            .placeholder { ProgressView().progressViewStyle(CircularProgressViewStyle()) }
/* If we scroll down before the image finished loading, we just cancel the loading */
            .cancelOnDisappear(true)
    }
/* Fetches the API data and append it to our binding data */
    private func loadWallpapers() {
        container.interactors.wallpapers.load(data: $wallpapers, page: currentPage)
    }


I don't think the network call is relevant here so I omitted it, please tell me if you need more explanations, I hope this better helps you understand how this View works.

I'll try:

Thanks for trying and showing the link. I want to spend some time to explore how memory is consumed in your code and if there is a way to reduce it.
Thank you, this is incredibly helpful. Please feel free to leave comments or an issue if you see something worth mentioning
Accepted Answer
Thanks to the runnable project and good explanations, I could have checked the behavior with relatively short time.
  • Memory consumption grows with the list size grows, even if each row has no image. But the rate is very low.

  • When images included, memory consumption grows very rapidly.

Below includes some guesses based on the observed fact:
  • LazyVStack adds rows on demand, that may update the view structure (so called view graph) which is causing the high memory consumption.

  • Each KFImage holds an instance of ImageBinding, which has a property image of type KFCrossPlatformImage?. That is not freed even if the row is disappeared. (KFCrossPlatformImage is UIImage on iOS.)

Thus, to reduce memory consumption, you may need to define your own Image view, of which you can purge the loaded image when disappeared.
Thank you so much for your time and for the explanations, it is extremely helpful.
I'll remove KingFisher and try to implement my own solution, I don't know how but I'll try :p

Thank you again for your time
How to limit memory usage of a list containing a large number of images / rows
 
 
Q