Avoiding SwiftUI Hang on loading list of images from CoreData

168 Views Asked by At

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?

2

There are 2 best solutions below

1
On BEST ANSWER

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.

0
On

I had a similar issue because I couldn't set some details for native LazyViews.
Therefore, I had to create my own LazyView, although it wasn't that convenient.

I can't provide the entire code, but here are some details:

  • Set an index to load items, for example, 5 items or 7 items.
  • If the second/third last item appears using .onAppear, then load the next item or the next group of items.
  • .onAppear will be called as soon as the item is shown, even if it's just 1% visible. Therefore, you may need to set some offset for that with additional code (for instance, if the item is shown more than 30%, load the next item)

p.s.
If you're not supporting iPads, a size of 1920x1080 is unnecessarily large for iPhones