Adjusting iPhone TableSearch Algorithm to Prevent UI Delay - iphone

Much of my code is based off of Apple's TableSearch example, however my app contains 35,000 cells that need to be searched rather than the few in the example. There isn't much documentation online about UISearchDisplayController since it is relatively new. The code I am using is as follows:
- (void)filterContentForSearchText:(NSString*)searchText {
/*
Update the filtered array based on the search text and scope.
*/
[self.filteredListContent removeAllObjects]; // First clear the filtered array.
/*
Search the main list for products whose type matches the scope (if selected) and whose name matches searchText; add items that match to the filtered array.
*/
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
for (Entry *entry in appDelegate.entries)
{
if (appDelegate.searchEnglish == NO) {
NSComparisonResult result = [entry.gurmukhiEntry compare:searchText options:(NSCaseInsensitiveSearch|NSDiacriticInsensitiveSearch) range:NSMakeRange(0, [searchText length])];
if (result == NSOrderedSame)
{
[self.filteredListContent addObject:entry];
}
}
else {
NSRange range = [entry.englishEntry rangeOfString:searchText options:NSCaseInsensitiveSearch];
if(range.location != NSNotFound)
{
[self.filteredListContent addObject:entry];
}
}
}}
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString {
[self filterContentForSearchText:searchString];
[self.view bringSubviewToFront:keyboardView];
// Return YES to cause the search result table view to be reloaded.
return YES;}
My problem is that there is a bit of a delay after each button is pressed on the keyboard. This becomes a usability issue because the user has to wait after typing in each character as the App searches through the array for matching results. How can this code be adjusted so that the user can continually type without any delays. In this case, it is ok for a delay in the time it takes for the data to reload, but it should not hold up the keyboard in typing.

Update:
One way to acccomplish searchching while typing without "locking up" the UI, is to use threads.
So you can call the the method that performs the sorting with this method:
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg
Which will keep it out of the main thread, allowing the UI update.
You will have to create and drain your own Autorealease pool on the background thread.
However, when you want to update the table you will have to message back to the main thread (all UI updates MUST be on the main thread):
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait
You could also get a little more control by using NSOperation/NSOperationQueue Or NSThread.
Note that implementing threads is fraught with peril. You will have to make sure your code is thread-safe and you may get unpredictable results.
Also, here are other stackoverflow answers that may help:
Using NSThreads in Cocoa?
Where can I find a good tutorial on iPhone/Objective-C multithreading?
Original answer:
Don't perform the search until the user presses the "search" button.
There is a delegate method you can implement to catch the pressing of the search button:
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar;

Related

Speed up search using dispatch_async?

I'm trying to speed up my app search , it get lags when there is a lot of data.
so i'm trying to split search Predicate on UI by using dispatch_async not dispatch_sync cause no different if I use it.
The problem is when i use dispatch_async, the app crash sometimes because [__NSArrayI objectAtIndex:]: index "17" beyond bounds.
I now this happened because lets say the first one still work and reload the tableView and continue search will change the array size depend on result so in this case "CRASH" :(
this is my code:
dispatch_async(myQueue, ^{
searchArray = [PublicMeathods searchInArray:searchText array:allData];
} );
if(currentViewStyle==listViewStyle){
[mytable reloadData];
}
and i've tried this :
dispatch_async(myQueue, ^{
NSArray *tmpArray = [PublicMeathods searchInArray:searchText array:allData];
dispatch_sync(dispatch_get_main_queue(), ^{
searchArray = tmpArray;
[mytable reloadData];
});
});
but in this case the lags still there.
Update -1- :
The search Predicate takes just 2ms :) after hard work :)
but the keyboard still lags when the user searches, so the only thing I do after get result is reload table "change in UI" this what I think make it lags,
So what I search for split this two operation "typing on keyboard & refresh UI".
Update -2- :
#matehat https://stackoverflow.com/a/16879900/1658442
and
#TomSwift https://stackoverflow.com/a/16866049/1658442
answers work like a charm :)
If searchArray is the array that is used as table view data source then this array must
only be accessed and modified on the main thread.
Therefore, on the background thread, you should filter into a separate temporary array first. Then you assign the temporary array to searchArray on the main thread:
dispatch_async(myQueue, ^{
NSArray *tmpArray = [PublicMeathods searchInArray:searchText array:allData];
dispatch_sync(dispatch_get_main_queue(), ^{
searchArray = tmpArray;
[mytable reloadData];
});
});
Update: Using a temporary array should solve the crash problem, and using a background thread helps to keep the UI responsive during the search. But as it turned out in the discussion, a major reason for the slow search might be the complicated search logic.
It might help to store additional "normalized" data (e.g. all converted to lower-case, phone numbers converted to a standard form, etc ...) so that the actual search can be done with
faster case-insensitive comparisons.
One solution might be to voluntarily induce a delay between searches to let the user type and let the search be performed asynchronously. Here's how:
First make sure your queue is created like this :
dispatch_queue_t myQueue = dispatch_queue_create("com.queue.my", DISPATCH_QUEUE_CONCURRENT);
Have this ivar defined in your class (and set it to FALSE upon initialization):
BOOL _scheduledSearch;
Write down this macro at the top of your file (or anywhere really, just make sure its visible)
#define SEARCH_DELAY_IN_MS 100
And instead of your second snippet, call this method:
[self scheduleSearch];
Whose implementation is:
- (void) scheduleSearch {
if (_scheduledSearch) return;
_scheduledSearch = YES;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)((double)SEARCH_DELAY_IN_MS * NSEC_PER_MSEC));
dispatch_after(popTime, myQueue, ^(void){
_scheduledSearch = NO;
NSString *searchText = [self textToSearchFor];
NSArray *tmpArray = [PublicMeathods searchInArray:searchText array:allData];
dispatch_async(dispatch_get_main_queue(), ^{
searchArray = tmpArray;
[mytable reloadData];
});
if (![[self textToSearchFor] isEqualToString:searchText])
[self scheduleSearch];
});
}
[self textToSearchFor] is where you should get the actual search text from.
Here's what it does :
The first time a request comes in, it sets the _scheduledSearch ivar to TRUE and tells GCD to schedule a search in 100 ms
Meanwhile any new search requests is not taken care of, because a search is going to happen anyway in a few ms
When the scheduled search happens, the _scheduledSearch ivar is reset to FALSE, so the next request is handled.
You can play with different values for SEARCH_DELAY_IN_MS to make it suit your needs. This solution should completely decouple keyboard events with workload generated from the search.
First, a couple notes on the code you presented:
1) It looks as if you're likely queuing up multiple searches as the user types, and these all have to run to completion before the relevant one (the most recent one) updates the display with the desired result set.
2) The second snippet you show is the correct pattern in terms of thread safety. The first snippet updates the UI before the search completes. Likely your crash happens with the first snippet because the background thread is updating the searchArray when the main thread is reading from it, meaning that your datasource (backed by searchArray) is in an inconsistent state.
You don't say if you're using a UISearchDisplayController or not, and it really doesn't matter. But if you are, one common issue is not implementing - (BOOL) searchDisplayController: (UISearchDisplayController *) controller shouldReloadTableForSearchString: (NSString *) filter and returning NO. By implementing this method and returning NO you are turning off the default behavior of reloading the tableView with each change to the search term. Instead you have opportunity to kick off your asynchronous search for the new term, and update the UI ([tableview reloadData]) only once you have new results.
Regardless of whether you're using UISearchDisplayController you need to take a few things into consideration when implementing your asynchronous search:
1) Ideally you can interrupt a search-in-progress and cancel it if the search is no longer useful (e.g. the search term changed). Your 'searchInArray' method doesn't appear to support this. But it's easy to do if your just scanning an array.
1a) If you can't cancel your search, you still need a way at the end of the search to see if your results are relevant or not. If not, then don't update the UI.
2) The search should run on a background thread as to not bog down the main thread and UI.
3) Once the search completes it needs to update the UI (and the UI's datasource) on the main thread.
I put together sample project (here, on Github) that performs a pretty inefficient search against a large list of words. The UI remains responsive as the user types in their term, and the spawned searches cancel themselves as they become irrelevant. The meat of the sample is this code:
- (BOOL) searchDisplayController: (UISearchDisplayController *) controller
shouldReloadTableForSearchString: (NSString *) filter
{
// we'll key off the _currentFilter to know if the search should proceed
#synchronized (self)
{
_currentFilter = [filter copy];
}
dispatch_async( _workQueue, ^{
NSDate* start = [NSDate date];
// quit before we even begin?
if ( ![self isCurrentFilter: filter] )
return;
// we're going to search, so show the indicator (may already be showing)
[_activityIndicatorView performSelectorOnMainThread: #selector( startAnimating )
withObject: nil
waitUntilDone: NO];
NSMutableArray* filteredWords = [NSMutableArray arrayWithCapacity: _allWords.count];
// only using a NSPredicate here because of the SO question...
NSPredicate* p = [NSPredicate predicateWithFormat: #"SELF CONTAINS[cd] %#", filter];
// this is a slow search... scan every word using the predicate!
[_allWords enumerateObjectsUsingBlock: ^(id obj, NSUInteger idx, BOOL *stop) {
// check if we need to bail every so often:
if ( idx % 100 == 0 )
{
*stop = ![self isCurrentFilter: filter];
if (*stop)
{
NSTimeInterval ti = [start timeIntervalSinceNow];
NSLog( #"interrupted search after %.4lf seconds", -ti);
return;
}
}
// check for a match
if ( [p evaluateWithObject: obj] )
{
[filteredWords addObject: obj];
}
}];
// all done - if we're still current then update the UI
if ( [self isCurrentFilter: filter] )
{
NSTimeInterval ti = [start timeIntervalSinceNow];
NSLog( #"completed search in %.4lf seconds.", -ti);
dispatch_sync( dispatch_get_main_queue(), ^{
_filteredWords = filteredWords;
[controller.searchResultsTableView reloadData];
[_activityIndicatorView stopAnimating];
});
}
});
return FALSE;
}
- (BOOL) isCurrentFilter: (NSString*) filter
{
#synchronized (self)
{
// are we current at this point?
BOOL current = [_currentFilter isEqualToString: filter];
return current;
}
}
i believe your crash is indeed solved by the embedding of the display of the UI element for which searchArray is the backing element in a call to GrandCentralDispatch inside of the other call (as you show in your updated original post). that is the only way to make sure you are not causing the elements of the array to change behind the scenes while the display of the items associated with it is taking place.
however, i believe if you are seeing lag, it is not so much caused by the processing of the array at 2ms or the reload that takes 30ms, but rather by the time it takes GCD to get to the internal dispatch_sync call on the main queue.
if, by this point, you have managed to get the processing of your array down to only 2ms in the worst case (or even if you've managed to get it down to less than 30ms, which is about the time it takes to process a frame in the main run loop at 30 fps), then you should consider abandoning GCD altogether in your effort to process this array. taking 2ms on the main queue to process your array is not going to cause any buggy behavior.
you may have lag elsewhere (i.e. if you are incrementing search results by trying to go out to the net to get the results, you may want to do the call and then process the response on your separate dispatch queue), but for the times you are talking about, this bit of processing doesn't need to be split out onto separate queues. for any hard-core processing that takes over 30ms, you should consider GCD.
I suspect your problem is that allData is shared between the main queue and the background queue. If you make a change in allData on the main queue, that may shorten allData in the background queue, causing an index that used to be valid to become invalid.
It's also possible that the problem is not allData itself, but some array within the objects in allData. Try setting a breakpoint on exceptions (in Xcode, open the Breakpoints source list, click the plus button at the bottom, and choose "Add Exception Breakpoint...") so you can see exactly where the error occurs.
In either case, you have two possible solutions:
Copy the offending object before using it in the search. This protects the background queue from changes in the main queue, but depending on what you need to copy, it may be difficult to get the changes back into the UI—you might have to match the copies back to their originals.
Use a lock (like #synchronized) or a per-object queue to ensure only one queue is using the object at a time. NSManagedObjectContext uses the latter approach for its -performBlock: and -performBlockAndWait: methods. It may be a little tricky to do this without blocking the main queue, though.
Try to modify your functions the next way:
function prototype;
- (void)searchInArray:searchText array:allData complete: (void (^)(NSArray *arr)) complete;
function itself
- (void)searchInArray:searchText array:allData complete: (void (^)(NSArray *arr)) complete {
NSArray * array = [NSArray new];
// function code
complete(array)//alarming that we have done our stuff
}
and when you are calling this function
dispatch_queue_t searchQueue = dispatch_queue_create("com.search",NULL);
dispatch_async(searchQueue,^{
[PublicMeathods searchInArray:searchText array:allData complete:^(NSArray *arr) {
searchArray = arr;
dispatch_async(dispatch_get_main_queue(), ^{
[myTable reloadData];
});
}];
});
Hope it will help you)
I found a simple solution with the same spirit of the solution presented by Matehad (wait some time and perform a search only if the user doesn't input anything else). Here it is:
Declare 2 global counters and a global string:
int keyboardInterruptionCounter1 = 0, int keyboardInterruptionCounter2 = 0 and NSString *searchTextGlobal
On the searchBar function do this:
-(void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText{
keyboardInterruptionCounter1++;
searchTextGlobal = searchText;//from local variable to global variable
NSTimeInterval waitingTimeInSec = 1;//waiting time according to typing speed.
//waits for the waiting time
[NSTimer scheduledTimerWithTimeInterval:waitingTimeInSec target:self selector:#selector(timerSearchBar:) userInfo:nil repeats:NO];
}
-(void)timerSearchBar:(NSTimer *)timer{
keyboardInterruptionCounter2++;
// enters only if nothing else has been typed.
if (keyboardInterruptionCounter2 == keyboardInterruptionCounter1) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
(unsigned long)NULL), ^(void) {
//do the search with searchTextGlobal string
dispatch_async(dispatch_get_main_queue(), ^{
//update UI
});
});
}
}
Explanation: The search is performed only if both counters are the same, this only happens if the user has typed and waited .52 sec without typing anything else. Instead, if the users types fast enough, then no query is done. The solution can be done with or without threading.
Martin R has posted a correct answer. The only thing to point out that instead of
dispatch_sync(dispatch_get_main_queue()
it should be
dispatch_async(dispatch_get_main_queue()
The complete code in Swift would be:
let remindersFetcherQueue = dispatch_queue_create("com.gmail.hillprincesoftware.remindersplus", DISPATCH_QUEUE_CONCURRENT)
dispatch_sync(remindersFetcherQueue) {
println("Start background queue")
estore.fetchRemindersMatchingPredicate(remindersPredicate) {
reminders in
// var list = ... Do something here with the fetched reminders.
dispatch_async(dispatch_get_main_queue()) {
self.list = list // Assign to a class property
self.sendChangedNotification() // This should send a notification which calls a function to ultimately call setupUI() in your view controller to do all the UI displaying and tableView.reloadData().
}
}
}

Order of events when loading a View in iOS

This one is driving me a little batty and my eyes are glossing over.
I have an app with a navigation controller.
View A has some input fields and a "Continue" button which loads View B
When I tap the "Back" button on the upper left of the navigation controller its resulting in events being fired in an order I'm not expecting/understanding
My tracing reveals ...
View B: viewWillDisappear
View A: viewWillAppear
View B: textFieldShouldEndEditing
EDIT -- more detail/code explaining my previously vague question
Conceptually the following approach has been working fine, and passed several rounds of QA testing.
In summary, I'm using the textFieldShouldEndEditing to validate textfields. If they aren't valid, I retain focus on the field and show them a message of whats wrong. All is good and validations work as the user attempts to go from field to field.
The condition that is problematic with the code below is if someone enters a partial value and then clicks BACK. All of the UITextFields in the entire app Freeze up (don't allow input) and in some cases the app crashes.
The approach I'm attempting which led me to post the initial Question was to create a private: BOOL isDisappearing;
Which I could check in viewWillDisappear (which in most cases fires PRIOR to textFieldShouldEndEditing), and if its YES I would short circuit the problematic code that is firing and freezing the UITextFields/app.
This is working in several views fine, but in 1 case where 'VIEW A:viewWillAppear' event fires before the textFieldShouldEndEditing below (VIEW B) - the isDisappearing gets set to 'NO' somehow and the problematic code is firing in textFieldShouldEndEditing
I hope this helps and you can follow. I find it hard to explain without code – but I've tried to trim it down to just what is relevant. I hope this is appropriate here – I'm pretty new to the community.
Code for VIEW B:
-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)rangereplacementString:(NSString *)string
{
// Enforce max lengths
// The return key from the keyboard counts as a character, so we have to exempt it
if (textField.tag == ROUTING_NUMBER_TAG)
{
NSUInteger newLength = [textField.text length] + [string length] - range.length;
return (newLength > 9 && ![string isEqualToString:#"\n"]) ? NO : YES;
}
else if (textField.tag == ACCOUNT_NUMBER_TAG)
{
NSUInteger newLength = [textField.text length] + [string length] - range.length;
return (newLength > 17 && ![string isEqualToString:#"\n"]) ? NO : YES;
}
return YES;
}
// textField validation
- (BOOL)textFieldShouldEndEditing:(UITextField *)textField
{
if (isDisappearing)
return YES;
//run fields through validators and display validation messages.
//IF THEY DON’T PASS VALIDATION IM "HOLDING THEM HOSTAGE" BY KEEPING THE FOCUS ON THE UITEXTFIELD (returning NO)
if (textField.tag == ROUTING_NUMBER_TAG )
{
if ([Utility isValidRoutingNumber:textField.text]== NO)
{
[[iToast makeText:NSLocalizedString(#"Enter valid routing number", #"")] show];
return NO;
}
else
{ //save it
extension.payment.routingNumber = routingNumber.text;
}
}
else if (textField.tag == ACCOUNT_NUMBER_TAG)
{
if ([Utility isValidAccountNumber:textField.text] == NO)
{
[[iToast makeText:NSLocalizedString(#"Enter valid account number", #"")] show];
return NO;
}
else
{ //save it
extension.payment.accountNumber = accountNumber.text;
}
}
return YES;
}
- (void)textFieldDidEndEditing:(UITextField *)textField
{
//I do nothing here except nulling out the 'activefield' var I use to autoscroll the uiscrollview as the user taps around from field to field
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
//reset the bool so that when they come back we're back to the 'normal' state and validation will again be checked in textFieldShouldEndEditing
isDisappearing = NO;
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
}
-(void)viewWillDisappear:(BOOL)animated
{
TRC_ENTRY
//set the bool to bypass validations in textFieldShouldEndEditing
isDisappearing = YES;
[super viewWillDisappear:animated];
}
Other than cases where the order of messages is clearly defined or strongly implied by names, you should avoid depending on any particular order. For example, you can reasonably expect -viewWillAppear to be called before -viewDidAppear for any given view, but don't expect one view's -viewWillAppear to be called in any particular order with respect to any message sent to a different view.
If you need help figuring out how to implement a particular feature without depending on order, please ask. But again, unless the order of invocation is documented or blatantly obvious from the method names, don't expect a particular order.
Update: I don't see exactly what's going wrong in the code you added, but perhaps a few suggestions will help:
Is your isDisappearing variable an instance variable of your view controller, a global variable, or what? If it's an instance variable, figure out how it's being changed. If it's a global variable, well... don't do that.
Be sure that you're heeding the warning in the docs to the effect that -textViewShouldEndEditing: is only advisory, and that the view may stop editing no matter what you return.
Try temporarily removing the iToast stuff. If the crash still happens, at least you've eliminated that as a source of problems. If it stops happening, you'll have narrowed your search.
Identify the cause of the crash. (This should really be first on the list.) Crashes don't just happen mysteriously -- there's a reason that it happens. Find that reason, and you're 85% done. Start by examining the stack trace when the crash occurs. If that doesn't provide enough clues, place a breakpoint somewhere before the line that causes the crash and start stepping until you crash. If all else fails, start logging messages to trace execution and monitor your assumptions.
What exactly is your view controller A doing in its -viewWillAppear method? Could that be part of the problem? Could you move that code to, say, -viewDidAppear instead?
Try doing [self.youTextField resignFirstResponder] on view B viewWillDisappear.
This might not be directly relevant to your question, but if you feel you don't fully understand lifetime of views, I suggest take a look here: What is the process of a UIViewController birth (which method follows which)?
It's a great explanation.

Delay UISearchbar parsing

I have a UISearchbar in my app. This is a dynamic search and as the user enters text, a remote database is searched via a remote API call (I think it is through REST).
The table view gets refreshed dynamically, as the user types. I am using NSXMLParser for parsing the XML results. (so 3 delegate methods; didStartElement, didEndElement)
In some cases, there are duplicate entries shown in the results
e.g. If user has typed YAH, it shows YAHOO 3-4 times. I'm not sure why.
How can I reduce the number of times the parsing is done, or how to delay the parsing, so that it does not make a request for every character entered/deleted by the user.
This, I am assuming, might fix the problem.
One thing you can do is introduce a delay before you send off the remote API call, instead of sending one query for every character.
// Whenever UISearchbar text changes, schedule a lookup
- (void)searchBar:(UISearchBar *)theSearchBar textDidChange:(NSString *)text {
// cancel any scheduled lookup
[NSObject cancelPreviousPerformRequestsWithTarget:self];
// start a new one in 0.3 seconds
[self performSelector:#selector(doRemoteQuery) withObject:nil afterDelay:0.3];
}
Here are the relevant parts of a method I use in one of my apps to remove duplicates from a web service result.
NSMutableArray *mutableResults = [[myResults mutableCopy] autorelease];
NSMutableSet *duplicates = [NSMutableSet set];
NSMutableIndexSet *indexesToRemove = [NSMutableIndexSet indexSet];
for (NSString *result in mutableResults)
{
if (![duplicates containsObject:result])
[duplicates addObject:result];
else
[indexesToRemove addIndex:[mutableResults indexOfObject:object]];
}
[mutableResults removeObjectsAtIndexes:duplicates];
return mutableResults;

UISearchDisplayController - how to display search result with only by scope button selected but empty search string

The UISearchDisplayController is very handy and implementing search is pretty straightforward.
However, I bump into problem when, in my app, I want to display search result with empty search string but selected scope button.
It seems like it's a must to enter some search string in order to get the search result table being initialized and displayed.
Is there any ways to display search result immediately after user has picked a scope but not entered search word yet?
Thanks
Bill
when you tap a new scope button the selectedScopeButtonIndex fires:
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchScope:(NSInteger)searchOption;
you could capture the title fire off your search here using:
[[self.searchDisplayController.searchBar scopeButtonTitles] objectAtIndex:searchOption]
Won't work on the initial scope index, but you could just fire off your search initially based on the last used selectedScopeButtonIndex
I was after the same thing and just found something in the Apple developer forums: The UISearchDisplayController is implemented in a way that the results table won't be shown until some text is entered. There's also a bug report about this: ID# 8839635.
I worked around it by putting a segmented control underneath the search bar, imitating the scope bar.
Here's a workaround that uses the scope buttons. The main thing is to add an extra character for the scope(s) that you want to show search results for automatically, but ensure that you remove it for the scope(s) that you do not want to do this.
You will need to implement searchBar:textDidChange as well as searchBar:selectedScopeButtonIndexDidChange:
// scope All doesn't do a search until you type something in, so don't show the search table view
// scope Faves and Recent will do a search by default
#define kSearchScopeAll 0
#define kSearchScopeFaves 1
#define kSearchScopeRecent 2
// this gets fired both from user interaction and from programmatically changing the text
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText{
[self initiateSearch];
}
- (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope{
NSString *searchText = self.searchDisplayController.searchBar.text;
// if we got here by selecting scope all after one of the others with no user input, there will be a space in the search text
NSString *strippedText = [searchText stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if ((selectedScope == kSearchScopeAll) && (strippedText.length == 0) && (searchText.length != 0)){
self.searchDisplayController.searchBar.text = #"";
} else {
[self initiateSearch];
}
}
-(void)initiateSearch{
NSString *searchText = self.searchDisplayController.searchBar.text;
NSInteger scope = self.searchDisplayController.searchBar.selectedScopeButtonIndex;
if ((searchText.length == 0) && (scope != kSearchScopeAll)){
self.searchDisplayController.searchBar.text = #" ";
}
switch (scope) {
case kSearchScopeAll:
[self searchAll:searchText];
break;
case kSearchScopeFaves:
[self searchFavorites:searchText];
break;
case kSearchScopeRecent:
[self searchRecents:searchText];
break;
default:
break;
}
}
// assume these trim whitespace from the search term
-(void)searchAll:(NSString *)searchText{
}
-(void)searchFavorites:(NSString *)searchText{
}
-(void)searchRecents:(NSString *)searchText{
}

Searching with a UISearchbar is slow and blocking the main thread

I have a Table with over 3000 entries and searching is very slow.
At the moment I am doing just like in the 'TableSearch' example code (but without scopes)
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString
{
[self filterContentForSearchText: searchString];
// Return YES to cause the search result table view to be reloaded.
return YES;
}
And the filterContentForSearchText method is as follows:
- (void) filterContentForSearchText:(NSString*)searchText
{
// Update the filtered array based on the search text
// First clear the filtered array.
[filteredListContent removeAllObjects];
// Search the main list whose name matches searchText
// add items that match to the filtered array.
if (fetchedResultsController.fetchedObjects)
{
for (id object in fetchedResultsController.fetchedObjects)
{
NSString* searchTarget = [tableTypeDelegate getStringForSearchFilteringFromObject:object];
if ([searchTarget rangeOfString:searchText
options:(NSCaseInsensitiveSearch|NSDiacriticInsensitiveSearch)].location != NSNotFound)
{
[filteredListContent addObject:object];
}
}
}
}
My question is twofold:
How do can I make the searching process faster?
How can I stop the search from blocking the main thread? i.e. stop it preventing the user from typing more characters.
For the second part, I tried "performSelector:withObject:afterDelay:" and "cancelPreviousPerformRequests..." without much success. I suspect that I will need to use threading instead, but I do not have much experience with it.
Answer for: "How do can I make the searching process faster?"
It seams that you are using core data results in your table. So it's better let core data do the filtering for you.
So create a new fetchedResultController using a NSPredicate for filtering.
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"name = %#", searchText];
[fetchRequest setPredicate:predicate];
using MATCHES instead of = let you define a regular expression comparison (for a case insensitive compare)
I ended up doing the searching as a NSOperation, so as not to block the main thread. I also did like Reinhard suggested and used a fetchedResultController.
There is a really good video on NSOperations on the apple developer site. I was called something like Advanced iPhone part 1 if I recall.