5

Update #4

  • I've Reordered this post to read a little more easily. What you will read below will detail a bug I've experienced using SwiftUI. I recently requested code level support from apple who confirmed same and asked that I reach out to feedback for resolution (which was also done, no answer yet).

The bug is this: After displaying a List or ForEach in a SwiftUI View, if you alter that view by changing the number of items listed, the UI locks up while it attempts to calculate the number of rows that have changed / need to change..

I have seen others who have experienced this bug in the Apple dev forums. Their temporary solution was to "set the array to blank" thereby clearing the list completely for about 100 milliseconds before modifying the listed dataset. This will avoid the lock up sufficiently for users iterating a List or ForEach using an Array of data.

Problem is, with CoreData, being used as is described in this post, there does not seem any way to clear the list in between letters being pushed (fetch requests).

In Update #3, there is a GitHub project which shows a sample of this issue with sample data.

Any input on workarounds is appreciated.

Update #3

Not good.. As described in this post, I was able to change from using CoreData to a local SQLite database file. My results were that the search was just as slow as using CoreData. I do not know what is going on here. But maybe it is something with rendering results to the SwiftUI output? Either way, searching and displaying a large amount of data seems impossible..

Ive posted a sample project which demonstrates this problem on GitHub, per J. Doe's request. This project can be found here

I hope someone can see what I'm doing wrong. I find it hard to believe that this is just a limitation of iOS..

Original Post

Any ideas?

I feel like I am missing something fundamental. My fetch request (code below) is super slow. I have tried to add an index to the CoreData model with negative improvement (suggestion from J. Doe below). I am thinking maybe I need to somehow add a fetchBatchSize declaration to the fetch request (figured this out - see update #2 below - no help), but with the property wrapper @FetchRequest in SwiftUI, there does not seem to be a way to do this.

The code below is working on a test dataset of about 5,000 records. In the search, each time the input is changed (with each letter typed), the search gets run again which drags the system to a halt (100+% on CPU and growing memory usage).

In previous apps, I have completed similar tasks, but those apps used an SQLite data file and were written in ObjC. In those instances, things were really fast, with more than 3 times this test dataset.

If anyone can point me in the right direction to speed up my CoreData fetch, I would be very appreciative. I do not want to have to go back to an SQLite file if I don't have to..

Thank you very much!

Using SwiftUI, here is my code:

struct SearchView: View {


    @Binding var searchTerm:String
    var titleBar:String

    var fetch: FetchRequest<MyData>
    var records: FetchedResults<MyData>{fetch.wrappedValue}

    init(searchTerm:Binding<String>, titleBar:String) {
        self._searchTerm = searchTerm
        self.titleBar = titleBar
        self.fetch = FetchRequest(entity: MyData.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \ MyData.header, ascending: true)], predicate: NSCompoundPredicate(type: .and, subpredicates: [ NSCompoundPredicate(type: .or, subpredicates: [NSPredicate(format: "%K CONTAINS[cd] %@", #keyPath(MyData.title),searchTerm.wrappedValue), NSPredicate(format: "%K CONTAINS[cd] %@", #keyPath(MyData.details),searchTerm.wrappedValue)]), NSPredicate(format: "%K == %@", #keyPath(MyData.titleBar),titleBar)])) //fetch request contains logic for module and search data - need to fix sort order later
    }

    var body: some View {


        List{

            Section(header: SearchBar(text: $searchTerm)) {

                ForEach(records, id: \.self) { fetchedData in

                    VStack {
                        NavigationLink(destination: DetailView(titleBar: fetchedData.title!, statuteData: fetchedData.details!, isFavorite: fetchedData.isFavorite)) {

                            HStack {
                                Text(fetchedData.header!)
                                    .font(.subheadline)

                                VStack(alignment: .leading) {
                                    Text(fetchedData.title!)
                                }
                                .scaledToFit()

                                Spacer()

                                if fetchedData.isFavorite {
                                    Image(systemName: "star.fill")
                                        .imageScale(.small)
                                        .foregroundColor(.yellow)
                                }
                            }
                        }
                    }
                }
            }.navigationBarTitle(Text("Search"))
        }
    }
}

Thank you for your assistance.

Update:

Before edit, I had reported another issue with storing data, however, that issue was resolved with the post below:

CoreData writing objects slow

Update #2:

My original question asked how to add the batch limit on my fetch to see if that helps. I was able to rewrite the fetch without using the FetchRequest wrapper, using NSFetchRequest and added the batch limit. It did nothing to help the situation..

Thanks again

JerseyDevel
  • 1,334
  • 1
  • 15
  • 34
  • 1
    You added indexes? Any reproduction project? – J. Doe Oct 14 '19 at 21:18
  • Found the - add index options - will try that and report my findings.. – JerseyDevel Oct 14 '19 at 21:46
  • added indexes for the two attributes in my predicate, both separately and together. unless I'm doing something wrong there, no help.. – JerseyDevel Oct 14 '19 at 21:55
  • Hey J. Doe, no, I have not indexed the table. Not sure how.. Also not sure what you mean by reproduction projects.. sorry – JerseyDevel Oct 15 '19 at 02:20
  • 1
    Copy pasting the code doens't work. I can have a look but only if the problem is easy to reproduce. One way to do that is that you make a project with the minimum code to reproduce the problem and upload it to github and add the github link here – J. Doe Oct 15 '19 at 07:28
  • 1
    I would try swapping round the two subpredicates in your AND compound predicate: do the strict == comparison of the titleBar first (quick) and the CONTAINS second as it is much slower (particularly as there are two subpredicates within that). – pbasdf Oct 15 '19 at 07:58
  • I tried testing this by removing the compound or predicate all together and just using the hard and. I also removed the sort descriptor. I think it improved marginally but still not acceptable even with those changes. I will try to work on a sample app to make a reproducible issue. The test records I’m working with (like the live data) contains about 17Mb worth of string data. This is what is being fetched over and over again. Maybe it’s something to do with core data’s intended purpose? Is it only for smaller amounts of data? It’s driving me crazy lol – JerseyDevel Oct 15 '19 at 11:22
  • @J. Doe - Ive created a reproducible project showing just this issue. When you run it, press on add to DB first so it can download my test data. Then click go to search screen and try to start typing. You'll see what Im talking about. Git Repo: https://github.com/louiskabo/Reproducible-.git – JerseyDevel Oct 17 '19 at 03:38

2 Answers2

5

I had the same issue of very slow scrolling in a list with fetched results from core data. It was just slow using swiftUI compared to my previous solution using UIKit (same data, same fetch). Besides having to use fetchOffset and fetchLimit I found out that a major performance issue was due to using NavigationLink. Without NavigationLink the performance was great, but with the NavigationLink it was not.

Searching for a solution I found this blog post by Anupam Chugh who wrote about this and also provided a solution that I copied below. I am grateful to him. It's his solution, not mine.

The key point is that when NavigationLink is used within list, the destination views are loaded immediately, even when the user hasn't navigated to that view. To overcome this, one has to make the destination view lazy.

In my case I selected a food in the master view and then showed a list of more than 180 properties of the selected food in the detail view...

Solution:

Create a new file with the following code

import SwiftUI

/// Creates a lazy view from view.
///
/// Helpfull for use of `NavigationLink` within `list`, where destination views    are loaded immediately even when the user hasn’t navigated to that view.
/// Embedding the destination view in LazyView makes the destination view lazy and speeds up performance significantly for long lists.
///
/// ```Swift
/// NavigationLink(destination: LazyView(Text("Detail Screen"))){
///    Text("Tap me to see detail screen!")
/// }
/// ```
///
/// Source: [Blog post by Anupam Chugh on Medium]( https://medium.com/better-programming/swiftui-navigation-links-and-the-common-pitfalls-faced-505cbfd8029b). Thank you!!!!
struct LazyView<Content: View>: View {
    let build: () -> Content
        init(_ build: @autoclosure @escaping () -> Content) {
        self.build = build
    }
    var body: Content {
        build()
    }
}

and use it like this:

NavigationLink(destination: LazyView(Text("Detail Screen"))){
   Text("Tap me to see detail screen!")
}

I hope this of help for others, too.

Community
  • 1
  • 1
user3687284
  • 2,206
  • 1
  • 15
  • 14
  • Great info, thank you. My lists contain a lot of nav links.. I am going to implement this. Appreciate the insight! – JerseyDevel Dec 30 '19 at 04:41
  • I’m wondering how you would modify this if the view you are navigating to takes parameters? If you are familiar, could you provide an example? I have lots of large lists and they seem clunky, no doubt because of this situation.. thx again. – JerseyDevel Dec 30 '19 at 11:10
  • 1
    I use it like this: ForEach(foods) { (food: Food) in NavigationLink(destination: LazyView(self.foodDetailsView(food: food)) ) { FoodNutrientsRowView(food: food, formatter: self.formatter) } } – user3687284 Dec 30 '19 at 13:38
  • 1
    and func foodDetailsView(food: Food) -> some View { FoodDetailsView(ingredientCollection: anIngredientCollection, food: food) } – user3687284 Dec 30 '19 at 13:39
  • Thanks so much. You just saved me a lot of time. I was having a very similar issue and never thought about the impact Nav links would have on performance! – Yng Kody Jun 06 '23 at 16:35
1

Here's the work around, which, may I add is completely unacceptable. It makes the app technically work, but is so slow, it feels like loading Windows 10 on an 8086. ridiculous.

Also, still no answer or even acknowledgement from Apple Feedback. And my code level support request was debited even though they stated they could not help me. Not happy..

Anyway, if you want to build an app that feels like its digging through mud, using CoreData and being able to search that data, here's your workaround..

1st: Create a hashable model of your data, or at least the part of the data that you will need to search and/or display:

struct MyDataModel: Hashable {
  let title: String
  let name: String
  let myData: String
}

2nd: Create an ObservableObject class that publishes a variable which contains an array of your data's model class you just created:

class MyData:ObservableObject {
  @Published var searchDataArray = [MyDataModel]()
}

3rd: make sure you push your environment variable to the views you plan to use this is: (This example is in my SceneDelegate.swift file

let myData = MyData()

and append .environmentObject(myData) to whatever view you need it in.

4th: Access the Env Var from your view: @EnvironmentObject var myData: MyData and load your fetch results to the published data array, i used this function to complete the task:

func arrayFiller(){ 

    if self.myData.searchDataArray.count > 0 {
        self.myData.searchDataArray.removeAll()
    }

    for item in self.fetchRequest {
        self.myData.searchDataArray.append(MyDataModel(title: item.title!, name: item.name!, myData: item:myData!))
    }
}

Finally, from the view you want to search, you can iterate your published env var and you can clear the array in between changes to the search criteria with a delay to avoid the bug.

ForEach(self.myData.searchDataArray, id: \.self) { fetchedItem in
    Text(fetchedItem.name)
}

Then, I use an .onReceive to watch my searchTerm variable for changes, wipe the published Array, wait 10 milliseconds and refill the array with the data that matches my search terms.

Its really slow and hideous. It works, but I don't think I could go anywhere near production with this mess.

JerseyDevel
  • 1,334
  • 1
  • 15
  • 34
  • It works much faster if you choose to only update the search term when "Search" or "Go" is pressed. So no more fluid list, it actually looks passable with the "search on Go" method being used. – JerseyDevel Oct 27 '19 at 18:11