Whiffer / SwiftUI-Core-Data-Test

Sample program to demonstrate how CoreData can be used with SwiftUI.
153 stars 19 forks source link

Entries are not fetched when Cells are loaded #9

Open GuyNamedMiguel opened 4 years ago

GuyNamedMiguel commented 4 years ago

Hi and thanks for this amazing project, it has really helped me to implement a CoreData logic that fits my project!

Ive been doing some testing now and faced the following issue: As far as Im concerned, the SwiftUI List is nothing more than a UITableView under the hood. From my older projects where I used UIKit, I know that the UITableView fetches Data as the cells need them, so when I scroll and a new cell needs to appear, this entry is fetched by using the IndexPath of said cell, which also means that when the UITableView is first loaded, only the first 10 or so entries are fetched and displayed in the visible cells.

This was the behavior I expected with SwiftUI List as well, but what I've found is that entries are not fetched as the List needs them to be displayed. The FetchRequest is done right at the beginning, when the List is first displayed, fetching ALL of the entries. The cells are still only added to the list as needed (I checked using the "onAppear" function), but the data is there from the beginning. This results in the List loading extremely slow every time it is displayed fresh. Of course not when there are 10 objects, but I tried with 1000, which isn't that big, and the List then takes a full 4 seconds to load, leaving the UI completely unresponsive in that time.

Do you know if there is any way to get the old behavior back so the FetchRequest only fetches the objects that are currently visible in form of cells?

Whiffer commented 4 years ago

I haven't experimented with large results sets yet, but it was my understanding that SwiftUI Lists are loaded lazily in iOS 14. I haven't done this either, but you might try setting the fetchBatchSize to a non-zero value in the NSFetchRequest.

GuyNamedMiguel commented 4 years ago

Im on iOS 14, newest Beta, and already tried to set the fetchBatchSize to different values. It works perfectly smooth in UIKit with the same fetch request. They must’ve changed anything about how the List behaves, the data is fetched all at once into memory and that data is used to fill the cells, which is completely useless :/

Whiffer commented 4 years ago

You should definitely submit a Feedback.

SpacyRicochet commented 3 years ago

To add to this;

What I found is that SwiftUI Lists handle two types of lists very well;

  1. Static form-type lists without a data source, where you use ForEach and Section to build up the view hierarchy. Since the ForEach are greedy, everything is build up up front. This is the approach used in these examples as well;
List() { // Note there's no data source in here.
  ForEach(sections) { section in
    Section(Text(section.name)) {
      ForEach(section.items) { item in
        Text(item.name)
      }
    }
  }
}
  1. Lists with a Single Section data source. The Lists now knows that is has a data source and that it should fetch its items lazily. This works by creating a UITableView under the hood and for each cell appearing, it peeks at the data source to fill it.
List(items) { item in // Note the data source in here.
  Text(item.name)
}

Now, the above approaches both don't support lazy fetching with sections! The first one obviously builds up everything up front. This breaks if you have a large dataset. You'd think the second one works, and a naive approach would give you the following;

struct EntriesView: View {
    @ObservedObject var entriesStorage: EntriesStorage // Wrapper around Core Data NSFetchedResultsController
    var body: some View {
        let sections = entriesStorage.sections
        List(sections) { sectionInfo in
            Section(header: Text(sectionInfo.name)) {
                ForEach(0..<sectionInfo.numberOfObjects) { index in
                    EntryResultRow(managedEntry: sectionInfo.entries()[index])
                }
            }
        }
    }
}

Sadly, what happens here is that each section is translated to one row. So each section is lazily loaded, but inside of the section all rows are immediately populated due to the ForEach. Even worse, if you decide you can live with that, all the rows are also put inside an implicit HStack. 😂

My conclusion so far is that datasets with lazy fetching of sectioned data is not possible yet. I haven't gotten around to creating a Feedback report for Apple yet, but I'll happily duplicate whichever ones are floating around already.

Whiffer commented 3 years ago

@SpacyRicochet so far none of my projects have needed lazy fetching, but I may have some time to play with it in the near future. On another note, I use Multiple Section data sources often and I think I have observed a bug in NSFetchedResultsController where the ‘name’ property of an NSFetchedResultsSectionInfo can be incorrect. I have submitted a Feedback and also created a complete project that demonstrates the issue. I made the project a macOS command line project in order to convince myself that the problem was not related to SwiftUI. Take a look if you can and let me know what you think. Thanks. My project is: https://github.com/Whiffer/FetchedResultsTest