Search code examples
swiftsortingfilterrealmcancellation

cancel filter and sorting of big data array


I'm building vocabulary app using realm. I have several objects of Vocabulary, which contains list of words. One vocabulary contains 45000 words

UI is build such way, that user can search by "BEGINSWITH", "CONTAINS" or "ENDSWITH" through word's title, if corresponding tab is selected.

As, there are several vocabularies, there are some words, that appear in several vocabularies, and I need to remove "duplicates" from UI.

When I do this filtering duplicates on resulted objects + sorting them alphabetically the UI of app freezes, till process completes.

My question is: 1) How can I cancel previous filter and realm filtering request, if tab changed (for example from Contains to Ends"? 2) How can I do all these filter/sorting requests in background, so UI will not freeze?

My code:

    let vocabularyPredicate = NSPredicate(format: "enabled == 1 AND lang_from CONTAINS[c] %@", self.language.value)

    self.vocabularies = Array(realm.objects(Vocabulary.self).filter(vocabularyPredicate).sorted(byKeyPath: "display_order"))

    let result = List<Word>()

    for object in self.vocabularies {
        let predicate = NSPredicate(format: "title \(selectedFilter.value)[c] %@", self.query.value.lowercased())
        result.append(objectsIn: object.words.filter(predicate))
    }

    self.words = Array(result).unique{$0.title}.sorted {
        (s1, s2) -> Bool in return s1.title.localizedStandardCompare(s2.title) == .orderedAscending
    }

selectedFilter.value is selected tab value: "BEGINSWITH", "CONTAINS" or "ENDSWITH" self.query.value.lowercased() - search query.

unique{$0.title} is extension method for array

extension Array {
    func unique<T:Hashable>(map: ((Element) -> (T)))  -> [Element] {
        var set = Set<T>() //the unique list kept in a Set for fast retrieval
        var arrayOrdered = [Element]() //keeping the unique list of elements but ordered
        for value in self {
            if !set.contains(map(value)) {
                set.insert(map(value))
                arrayOrdered.append(value)
            }
        }

        return arrayOrdered
    }
}

Actually, realm search is pretty fast, but because of looping through vocabularies and filtering duplicates + sorting alphabetically operations through array of objects - request is freezing for 1-2 seconds.

UPDATE, based on EpicPandaForce and Manuel advices:

I have lurked one more time, and it appeared, that .distinct(by: [keypath]) is already presented in Results in new version of RealmSwift.

I have changed filter/sorting request to

realm.objects(Word.self).filter(vocabularyPredicate).distinct(by: ["title"]).sorted(byKeyPath: "title", ascending: true) 

works better know, but I want to ensure, UI will not freeze anyway, by passing objects bettween background thread and UI thread. I have updated adviced construction to:

DispatchQueue.global(qos: .background).async {
        let realm = try! Realm()

        let cachedWords = CashedWords()

        let predicate = NSPredicate(format: "enabled == 1")
        let results = realm.objects(Word.self).filter(predicate).distinct(by: ["title"]).sorted(byKeyPath: "title", ascending: true)

        cachedWords.words.append(objectsIn: results)

        try! realm.write {
            realm.add(cachedWords)
        }

        let wordsRef = ThreadSafeReference(to: cachedWords)

        DispatchQueue.main.async {
            let realm = try! Realm()

            guard let wordsResult = realm.resolve(wordsRef) else {
                return
            }

            self.words = Array(wordsResult.words)

            if ((self.view.window) != nil) {
                self.tableView.reloadData()
            }
        }

        print("data reload finalized")
    }

Solution

  • 1) How can I cancel previous filter and realm filtering request, if tab changed (for example from Contains to Ends"?

    You could create an NSOperation to perform the task and check if it's been cancelled between each of the steps (fetch, check isCancelled, filter, check isCancelled, sort). You won't get to cancel it immediately, but it could improve your performance. It also depends on which of those three steps (fetch, filter, sort) is taking longer...

    2) How can I do all these filter/sorting requests in background, so UI will not freeze?

    You could run that operation inside a new NSOperationQueue. Or just use GCD, dispatch a block to a background queue, create a Realm instance in the block and run your code there, then dispatch the results back to the main queue to update the UI. Something like this:

    DispatchQueue.global(qos: .userInitiated).async {
    
        guard let realm = try? Realm() else {
            return // maybe pass an empty array back to the main queue?
        }
    
        // ...
        // your code here
        // ...
    
        let words = Array(result).unique{$0.title}.sorted {
            (s1, s2) -> Bool in return s1.title.localizedStandardCompare(s2.title) == .orderedAscending
        }
        // Can't pass Realm objects directly across threads
        let wordReferences = words.map { ThreadSafeReference(to: $0) }
    
        DispatchQueue.main.async {
            // Resolve references on main thread
            let realm = try! Realm()
            let mainThreadWords = wordReferences.flatMap { realm.resolve($0) }
            // Do something with words
            self.words = mainThreadWords
        }
    }
    

    Additionally, you should try to optimize your query:

    let predicate = NSPredicate(format: "vocabulary.enabled == 1 AND vocabulary.lang_from CONTAINS[c] %@ AND title \(selectedFilter.value)[c] %@", self.language.value, self.query.value.lowercased())
    let words = realm.objects(Word.self).filter(predicate).sorted(byKeyPath: "title")
    let wordsReference = ThreadSafeReference(words)
    // resolve this wordsReference in the main thread