5
votes

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

2
You added indexes? Any reproduction project?J. Doe
Found the - add index options - will try that and report my findings..Lkabo
added indexes for the two attributes in my predicate, both separately and together. unless I'm doing something wrong there, no help..Lkabo
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 hereJ. Doe
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

2 Answers

2
votes

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.

1
votes

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.