How to read and filter large Realm dataset in SwiftUI?

▼魔方 西西 提交于 2021-02-11 12:24:47


I'm storing ~100.000 dictionary entries in a realm database and would like to display them. Additionally I want to filter them by a search field. Now I'm running in a problem: The search function is really inefficient although I've tried to debounce the search.

View Model:

class DictionaryViewModel : ObservableObject {

    let realm = DatabaseManager.sharedInstance

    @Published var entries: Results<DictionaryEntry>?
    @Published var filteredEntries: Results<DictionaryEntry>?
    @Published var searchText: String = ""
    @Published var isSearching: Bool = false
    var subscription: Set<AnyCancellable> = []
    init() {
            .debounce(for: .milliseconds(800), scheduler: RunLoop.main) // debounces the string publisher, such that it delays the process of sending request to remote server.
            .map({ (string) -> String? in
                if string.count < 1 {
                    self.filteredEntries = nil
                    return nil

                return string
            .compactMap{ $0 } 
            .sink { (_) in   
            } receiveValue: { [self] (searchField) in
                filter(with: searchField)
            }.store(in: &subscription)


    public func fetch(){
        self.entries = DatabaseManager.sharedInstance.fetchData(type:   DictionaryEntry.self).sorted(byKeyPath: "pinyin", ascending: true)
        self.filteredEntries = entries

    public func filter(with condition: String){
        self.filteredEntries = self.entries?.filter("pinyin CONTAINS[cd] %@", searchText).sorted(byKeyPath: "pinyin", ascending: true)

In my View I'm just displaying the filteredEtries in a ScrollView

The debouncing works well for short text inputs like "hello", but when I filter for "this is a very long string" my UI freezes. I'm not sure whether something with my debounce function is wrong or the way I handle the data filtering in very inefficient.

EDIT: I've noticed that the UI freezes especially when the result is empty.

EDIT 2: The .fetchData() function is just this here:

func fetchData<T: Object>(type: T.Type) -> Results<T>{
    let results: Results<T> = realm.objects(type)
    return results

All realm objects have a primary key. The structure looks like this:

@objc dynamic var id: String = NSUUID().uuidString
@objc dynamic var character: String = ""
@objc dynamic var pinyin: String = ""
@objc dynamic var translation: String = ""

override class func primaryKey() -> String {
    return "id"

EDIT 3: The filtered results are displayed this way:

    LazyVGrid(columns: gridItems, spacing: 0){
                    if (dictionaryViewModel.filteredEntries != nil)  {
                                 ForEach(dictionaryViewModel.filteredEntries!){ entry in
                    } else {
                        Text("No results found")