Filtering results causes app to hang and stutter

Hi there,
I am working on an ios todo list that can be sorted and filtered in various way. I created an observableobject that can be intialized with different variables so that as the variables change, the results are updated and passed on to a list. However when the variables change and the result is updated, it maxes out the phone cpu and make the whole application hang. Any Advice?

Here is my code:

final class TaskHelper: ObservableObject
{
    private var taskToken: NotificationToken!
    var taskResults: Results<Tasks>
    
    init(filters: [String], quadrant: String, sort:Int, autoAdvance: Bool, focus: Int)
    {
        
        let realm = try! Realm()
        
        if filters == [NSLocalizedString("_none", comment: "")] || filters.isEmpty || filters == [] || filters == [""]
        {
            taskResults = realm.objects(Tasks.self)
                .sorted(by: sort.getSortDescriptor())
                .getfocus(quadrant: quadrant, focus: focus, autoAdvance: autoAdvance)
        }
        else
        {
            taskResults = realm.objects(Tasks.self)
                .sorted(by: sort.getSortDescriptor())
                .getfocus(quadrant: quadrant, focus: focus, autoAdvance: autoAdvance)
                .filter(NSPredicate(format: "filter IN %@", filters as CVarArg))
        }
    
        
        
        lateInit()
    }
    
    func lateInit(){
        taskToken = taskResults.observe { [weak self] _ in
            self?.objectWillChange.send()
        }
    }
    
    deinit {
        taskToken.invalidate()
    }
    
    var taskArray: [Tasks]
    {
        taskResults.map(Tasks.init)
    }
    
}
ScrollView{
                    LazyVStack{
                        ForEach(taskHelper.taskResults)
                        { task in
                            TaskRow(task: task, quadrant: quadrant)
                        }
                    }
                }

If your dataset is large, one of the things that can affect negatively affect performance is to use high level Swift functions on Realm collections.

Realm Results are Live Objects which are lazily-loaded - meaning that huge datasets will have very low memory impact. However, as soon as Swift high level functions are used, that lazy-loading stops and ALL of the data is loaded into memory which can overwhelm it, as in this case.

For example, this function loads everything up into memory

.sorted(by: sort.getSortDescriptor())

whereas the Realm sorted(by: function

.sorted(byKeyPath: "name", ascending: false)

is memory friendly and maintains the lazy-loading nature of Realm.

Then this is a quandary

.getfocus(quadrant: quadrant, focus: focus, autoAdvance: autoAdvance)

as it’s not a Realm function at all as far as I know.

What is that? What happens when it’s removed and the same or similar actions are performed?

Thanks for your response Jay.
The problem is that there are various ways the list can be sorted. Is there any way to let user change the sort keypath? I used the sort.getSortDescriptor() to provide the keypath as chosen by the user. And the getFocus is an extension on RealmResults to filter the list based on user input e.g quadrant. I comment it out as you suggest, but my app was just as slow unfortunately.

@Deji_Apps Realm sorting is flexible so yes, you can sort by any keypath(s) of the object.

You may be doing that now but as is, it’s invoking a high level Swift sort which may be part of the issue.

As an proof-of-concept, we have a Realm with 100,000 Person objects. Each Person Object has a name, address etc - pretty straight forward. If I call this function

let results = realm.objects(PersonClass.self).sorted(byKeyPath: "name")

The results return in about a half-second. Noting we are using the Realm sort .sorted(byKeyPath:, not the Swift sort .sorted(by:.

Note that when posting questions, having “mystery code” can make understanding the issue difficult. As mentioned above, there was an extension .getfocus(quadrant: quadrant, on your object which we don’t know what that does. Good it was removed.

Likewise there’s this code

taskToken = taskResults.observe { [weak self] _ in
   self?.objectWillChange.send()
}

Which adds an additional air of mystery as we don’t know what self?.objectWillChange.send() does either.

It may be an issue though because it will fire after all the results are loaded so perhaps it’s doing something on the UI or working with the data in some way causing the app to become sluggish.

Try a little test using async/await calls. Replace your TaskHelper temporarily with this to see how long it takes to populate results with all of your tasks.

final class TaskHelper: ObservableObject
{
    var taskResults: Results<Tasks>
    
    init(filters: [String], quadrant: String, sort:Int, autoAdvance: Bool, focus: Int)  {
        let realm = try! Realm()

        Task {
           let startTime = Date()
           let results = try await realm.objects(Tasks.self)
           let elapsed = Date().timeIntervalSince(startTime)
           print("Load took: \(elapsed * 1000) ms")
        }
    }  
}

Then, add a Realm sort, and specify some simple property to sort by (where ‘some path’ is)

try await realm.objects(Tasks.self).sorted(byKeyPath: "some path")

and run it again

Report back your findings.

Unfortunately I have not dove into Swift’s async/await yet, so I don’t fully understand it. When I tried the code you posted, it crashes with the error : "Thread 2: “Realm accessed from incorrect thread.” However I got some results from this if it helps:

DispatchQueue.main.async {
            do {
                let startTime = Date()
                let realm = try Realm()
                self.taskResults = realm.objects(Tasks.self)
                let elapsed = Date().timeIntervalSince(startTime)
                print("Load took: \(elapsed * 1000) ms")
            }
            catch{
                print("Realm error: \(error)")
            }
        }

Without sorting or filtering, Load took: 0.030040740966796875 ms
With sorting and filtering, Load took: 0.1760721206665039 ms

Also you are absolutely right about mystery code, I’ll try to provide a better explanation for them.
Because the user should be able to change how the list is filtered and sorted at the click of a button, I wrote extensions to be able to dynamically provide the sort keypaths.

func getSortDescriptor() -> [RealmSwift.SortDescriptor]
    {
        if self == 0
        {
            return [SortDescriptor(keyPath: "complete", ascending: true), SortDescriptor(keyPath: "position", ascending: true)]
        }
        if self == 1
        {
            return [SortDescriptor(keyPath: "complete", ascending: true), SortDescriptor(keyPath: "task", ascending: true)]
        }
        else if self == 2
        {
            return [SortDescriptor(keyPath: "complete", ascending: true), SortDescriptor(keyPath: "date_added", ascending: true)]
        }
        else if self == 3
        {
            return [SortDescriptor(keyPath: "complete", ascending: true), SortDescriptor(keyPath: "date_added", ascending: false)]
        }
        else if self == 4
        {
            return [SortDescriptor(keyPath: "complete", ascending: true), SortDescriptor(keyPath: "due_date", ascending: false)]
        }
        else
        {
            return [SortDescriptor(keyPath: "complete", ascending: true), SortDescriptor(keyPath: "due_date", ascending: true)]
        }
    }

and the getFocus is an extension on RealmResults to filter the results based on user choice:

    func getfocus(quadrant: String, focus: Int, autoAdvance: Bool) -> RealmSwift.Results<Tasks>
    {
        // MARK: overdue only
        if focus == 0 //overdue only
        {
            if autoAdvance
            {
                if quadrant == "q1"
                {
                    let list = self as! Results<Tasks>
                    return list.where{
                        ($0.quadrant == quadrant &&
                         $0.due_date < Date() &&
                         $0.due_date != nil &&
                         $0.complete == false  &&
                         $0.deleted == false)
                        
                        ||
                        
                        ($0.quadrant == "q2" &&
                         $0.due_date < Date() &&
                         $0.due_date != nil &&
                         $0.complete == false &&
                         $0.deleted == false)}
                }
                else if quadrant == "q3"
                ...
            }
            else
            {
                let list = self as! Results<Tasks>
                return list.where{
                    $0.quadrant == quadrant &&
                    $0.due_date < Date() &&
                    $0.due_date != nil &&
                    $0.complete == false &&
                    $0.deleted == false}
            }
        }
        
        // MARK: due today only
        else if focus == 1
        {
          ...
        }
    }

Does this help understand my situation better? I am still new to coding for IOS and I appreciate your patient.

Based on those results, which is super fast and about what I expected, that would not cause the app to hang and stutter, so we can eliminate Realm from the equation.

1 -

Per the above, the sorting is being done using high level Swift functions - you will be better off using Realm sorting functions. Also, the sorting is a bit odd and SortDescriptors are not really needed - I would simplify that something like

let taskResults = self.getTasks()

which calls the following - I added a sortType to switch on as self == 1 was unclear

func getTasks() -> Results<Task> {
   switch self.sortType {
      case 0:
        let results = realm.results(Task.self).sorted(byKeyPath: "position", ascending: true
        return results
     case 1:
       ...
}

2 -

It’s not really clear why Results are being extended as the code doesn’t really extend the capability of results - it’s really just filtering data. It’s not ‘wrong’ but it may be more managable to just filter the data as needed using Realm filters

let quads = [quadrant, “q2”, “q3”]
let results = realm.objects(Task.self).where { $0.quadrant.in(quads) && $0.due_date < Date() && …}

I want to stress that this is all just guesswork - clearly the initial issue described has nothing to do directly with Realm per se - loading your data in 0.03 is pretty darn quick.

Oh - btw… I just noticed the speed test you posted above.

The code I posted above to test how long it took Realm to load using the async/await so the end time would calculate once the data was fully loaded. Your version of the code doesn’t wait for the data to fully load (it’s loading asynchronously) so it won’t be accurate.

If you want to do it that way, add an observer to the results and when that fires (the .initial) the results are loaded and the total time can be calculated. See my SO answer to this question for example code.

Either way though, I am confident the performance issues are not tied directly to Realm.