Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to filter large array in iOS swift 2 for uisearchbar

I am having a UISearchBar with more than 80000 elements in an array and I have to filter this array according to the user input.

But while typing in search view its working very slow means its taking too much time for typing values in keyboard.

func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {

    if searchText.characters.count == 0 {
        searchActive = false
    } else {
        searchActive = true;
        filtered.removeAllObjects()

        dispatch_to_background_queue {
            for sumber in self.data {
                let nameRange: NSRange = sumber.rangeOfString(searchText, options: [NSStringCompareOptions.AnchoredSearch,NSStringCompareOptions.CaseInsensitiveSearch])
                if nameRange.location != NSNotFound {
                    self.filtered.addObject(sumber)
                }
            }//end of for

            self.dispatch_to_main_queue {
                /* some code to be executed on the main queue */
                self.tableView.reloadData()
            }
        } //end of dispatch
    }
}

func dispatch_to_main_queue(block: dispatch_block_t?) {
    dispatch_async(dispatch_get_main_queue(), block!)
}

func dispatch_to_background_queue(block: dispatch_block_t?) {
    let q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
    dispatch_async(q, block!)
}
like image 239
ritesh Avatar asked Jan 07 '23 14:01

ritesh


2 Answers

There are two approaches to combine here for the best result:

First, keep long-running operations off the main (UI) thread

You can dispatch the filtering to a background thread using dispatch_async, or even to a background thread after some delay using dispatch_after.

Second, don't filter the array immediately after every key press

It's a waste of time because usually the user will type several keys before waiting to see what pops up. You want to therefore delay the filtering operation, and only perform it after some small amount of time has passed since the last key press. This is called "debouncing".

Here's a neat way to do all of this in Swift:

func debounce(delay:NSTimeInterval, queue:dispatch_queue_t, action: (()->())) -> (()->()) {

    var lastFireTime:dispatch_time_t = 0
    let dispatchDelay = Int64(delay * Double(NSEC_PER_SEC))

    return {
        lastFireTime = dispatch_time(DISPATCH_TIME_NOW,0)
        dispatch_after(
            dispatch_time(
                DISPATCH_TIME_NOW,
                dispatchDelay
            ),
            queue) {
                let now = dispatch_time(DISPATCH_TIME_NOW,0)
                let when = dispatch_time(lastFireTime, dispatchDelay)
                if now >= when {
                    action()
                }
        }
    }
}

class ViewController {

    lazy var debouncedFilterArray : () -> () = debounce(0.3, queue: dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), action: self.filterArray)

    func filterArray() {
        // do filtering here, but don't call this function directly            
    }
}

The debounce function itself returns a function that when called will exhibit this "debouncing" behaviour, running no more often than the delay interval passed to it.

To use, simply call debouncedFilterArray(). It will in turn call filterArray, but always on a background thread and never more often than every 0.3 seconds.

like image 59
Craig McMahon Avatar answered Jan 14 '23 11:01

Craig McMahon


I want to add a couple of thoughts.

You already seem to do async processing, which is great. It won't make the search faster, but the app keeps responsive. Consider making it stoppable. If the user types three letters, you will queue up three searches and will get the relevant results only after the last run finished. This could be done using some sort of a boolean stop flag that gets checked within the search. If a new search is started, kill the old one first.

Show partial results. The user won't be watching at thousands of cells at once, but only at the first 20 or so. Depending on the order of your input and output, this may be very easy to do and fast as hell.

Build on your previous search. Searching for "Ab" will only be successful if searching for "A" (or "b" for that matter if the search wasn't anchored) was successful at well. So if your last search was a substring from your current search, take the output array of your previous search as an input. Obviously, be careful with stopped searches here.

Check if performance is really as bad. Do you run with optimizations switched on? The debug mode might be considerable slower, but that wouldn't matter.

Where does the data come from? That's a rather huge amount of data to keep around in memory. If it's coming from a database, using database functions might be easier (and most words above still comply).

Still too slow? Index your data set. If you know upfront which elements contain "A", the number of needed searches could drop significantly. And you'd have the results for the first search already

As you're using anchored search, working on a sorted array could give a much better performance characteristic. Just find the first and last element of your search term with binary search and use that range. Maybe without even copying into a new array. This approach puts some workload upfront (maybe before the user even started typing). If your search data is within larger objects, some sort of index tables would do.

like image 25
Eiko Avatar answered Jan 14 '23 12:01

Eiko