Search code examples
iosswiftuicore-datafreezelazyhgrid

Avoiding SwiftUI Hang on loading list of images from CoreData


The following has a SwiftUI Hang for several seconds.

This code is copy-pasted and makes up one of the tabs of my simple app. Hang occurs once when first showing this tab.

Number of images is ~150; image size 1920x1080.

struct PhotosView : View
{
    @FetchRequest(sortDescriptors: [SortDescriptor(\Photo.date, order: .reverse)], animation: .default)
    private var photos: FetchedResults<Photo>

    @State private var gridHeight = 180.0

    var body: some View
    {
        VStack
        {
            ScrollView(.horizontal)
            {
                LazyHGrid(rows: [GridItem(.fixed(gridHeight))], alignment: .top, spacing: 0)
                {
                    ForEach(photos)
                    { photo in
                        if let data = photo.filteredData
                        {
                            Image(data: data)
                                .resizable()
                                .scaledToFit()
                        }
                    }
                }
                .frame(height: 200)
            }
        }
    }
}

I thought that using the LazyHGrid only a handful would be loaded initially (only one and almost a half fit on screen currently).

How I can improve performance?


Solution

  • By default the fetch will load every filteredData into memory even if it is not being displayed. You need to use a custom NSFetchRequest with includesPropertyValues set to false and then make a subview that takes the object and accesses filteredData causing it only to be loaded for that one object when it appears (body will be called at this time). You would also benefit from a predicate that searches for non-nil data that would help avoid the if in your body which can cause slow downs. E.g. something like:

    struct PhotoView: View {
        @ObservedObject var photo: Photo
    
        var body: View {
            Image(data: photo.filteredData!)
                .resizable()
                .scaledToFit()
        }
    }
    
    // this could be moved to a lazy var in an extension of Photo
    let fetchRequest: NSFetchRequest<Photo> = {
        let fr = Photo.fetchRequest()
        fr.predicate = NSPredicate(format: "filteredData != nil")
        fr.includesPropertyValues = false
        fr.sortDescriptors = [NSSortDescriptor(\Photo.date, ascending: false)]
        return fr
    }
    
    struct PhotosView : View
    {
        @FetchRequest(fetchRequest: fetchRequest, animation: .default)
        private var photos: FetchedResults<Photo>
    
    ...
            ForEach(photos) { photo in
                PhotoView(photo: photo)
            }
    ...
    

    See my answer to another question for more detail.